Skip to content

Commit 3e4c1a5

Browse files
committed
feat(sdk-core): added OFC BitGo signing on wallet and coins object
allow wallet and coins object to sign using the BitGo key if the passphrase is not provided during signing Ticket: WCN-217-2
1 parent ded3b89 commit 3e4c1a5

4 files changed

Lines changed: 262 additions & 4 deletions

File tree

modules/sdk-core/src/coins/ofc.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
SignTransactionOptions,
1616
VerifyAddressOptions,
1717
VerifyTransactionOptions,
18+
Wallet,
1819
} from '../';
1920

2021
export class Ofc extends BaseCoin {
@@ -104,6 +105,26 @@ export class Ofc extends BaseCoin {
104105
throw new MethodNotImplementedError();
105106
}
106107

108+
/**
109+
* Signs a message using a trading wallet's BitGo Key
110+
* @param wallet - uses the BitGo key of this trading wallet to sign the message remotely in a KMS
111+
* @param message
112+
*/
113+
async signMessage(wallet: Wallet, message: string): Promise<Buffer>;
114+
/**
115+
* Signs a message using the private key
116+
* @param key - uses the private key to sign the message
117+
* @param message
118+
*/
119+
async signMessage(key: { prv: string }, message: string): Promise<Buffer>;
120+
async signMessage(keyOrWallet: { prv: string } | Wallet, message: string): Promise<Buffer> {
121+
if (!(keyOrWallet instanceof Wallet)) {
122+
return super.signMessage(keyOrWallet as { prv: string }, message);
123+
}
124+
const signatureHexString = await (keyOrWallet as Wallet).toTradingAccount().signPayload({ payload: message });
125+
return Buffer.from(signatureHexString, 'hex');
126+
}
127+
107128
/** @inheritDoc */
108129
auditDecryptedKey(params: AuditDecryptedKeyParams) {
109130
throw new MethodNotImplementedError();

modules/sdk-core/src/coins/ofcToken.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
SignTransactionOptions as BaseSignTransactionOptions,
1010
SignedTransaction,
1111
ITransactionRecipient,
12+
Wallet,
1213
} from '../';
1314
import { isBolt11Invoice } from '../lightning';
1415

@@ -18,7 +19,8 @@ export interface SignTransactionOptions extends BaseSignTransactionOptions {
1819
txPrebuild: {
1920
payload: string;
2021
};
21-
prv: string;
22+
prv?: string;
23+
wallet?: Wallet;
2224
}
2325

2426
export { OfcTokenConfig };
@@ -107,15 +109,25 @@ export class OfcToken extends Ofc {
107109
}
108110

109111
/**
110-
* Assemble keychain and half-sign prebuilt transaction
112+
* Signs a half-signed OFC transaction.
113+
* Signs the transaction remotely using the BitGo key if prv is not provided.
111114
* @param params
112115
* @returns {Promise<SignedTransaction>}
113116
*/
114117
async signTransaction(params: SignTransactionOptions): Promise<SignedTransaction> {
115118
const txPrebuild = params.txPrebuild;
116119
const payload = txPrebuild.payload;
117-
const signatureBuffer = (await this.signMessage(params, payload)) as any;
118-
const signature: string = signatureBuffer.toString('hex');
120+
121+
let signature: string;
122+
if (params.wallet) {
123+
signature = await params.wallet.toTradingAccount().signPayload({ payload, walletPassphrase: params.prv });
124+
} else if (params.prv) {
125+
const signatureBuffer = (await this.signMessage({ prv: params.prv }, payload)) as any;
126+
signature = signatureBuffer.toString('hex');
127+
} else {
128+
throw new Error('You must pass in either one of wallet or prv');
129+
}
130+
119131
return { halfSigned: { payload, signature } } as any;
120132
}
121133

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* @prettier
3+
*/
4+
import sinon from 'sinon';
5+
import 'should';
6+
import { Wallet } from '../../../../src';
7+
8+
describe('Wallet - OFC signTransaction', function () {
9+
let wallet: Wallet;
10+
let mockBitGo: any;
11+
let mockBaseCoin: any;
12+
let mockWalletData: any;
13+
14+
beforeEach(function () {
15+
mockBitGo = {
16+
url: sinon.stub().returns('https://test.bitgo.com'),
17+
post: sinon.stub(),
18+
get: sinon.stub(),
19+
setRequestTracer: sinon.stub(),
20+
};
21+
22+
mockBaseCoin = {
23+
getFamily: sinon.stub().returns('ofc'),
24+
url: sinon.stub().returns('https://test.bitgo.com/wallet'),
25+
keychains: sinon.stub(),
26+
supportsTss: sinon.stub().returns(false),
27+
getMPCAlgorithm: sinon.stub(),
28+
presignTransaction: sinon.stub().resolvesArg(0),
29+
keyIdsForSigning: sinon.stub().returns([0]),
30+
signTransaction: sinon.stub().resolves({ halfSigned: { payload: 'test', signature: 'aabbcc' } }),
31+
};
32+
33+
mockWalletData = {
34+
id: 'test-wallet-id',
35+
coin: 'ofcusdt',
36+
keys: ['user-key', 'backup-key', 'bitgo-key'],
37+
multisigType: 'onchain',
38+
enterprise: 'ent-id',
39+
};
40+
41+
wallet = new Wallet(mockBitGo, mockBaseCoin, mockWalletData);
42+
});
43+
44+
afterEach(function () {
45+
sinon.restore();
46+
});
47+
48+
it('should pass wallet instance to baseCoin.signTransaction', async function () {
49+
const txPrebuild = { txInfo: { payload: '{"amount":"100"}' } } as any;
50+
const prv = 'test-prv';
51+
52+
await wallet.signTransaction({ txPrebuild, prv });
53+
54+
mockBaseCoin.signTransaction.calledOnce.should.be.true();
55+
const callArgs = mockBaseCoin.signTransaction.getCall(0).args[0];
56+
callArgs.wallet.should.equal(wallet);
57+
});
58+
59+
it('should pass prv to baseCoin.signTransaction when provided directly', async function () {
60+
const txPrebuild = { txInfo: { payload: '{"amount":"100"}' } } as any;
61+
const prv = 'test-prv';
62+
63+
await wallet.signTransaction({ txPrebuild, prv });
64+
65+
const callArgs = mockBaseCoin.signTransaction.getCall(0).args[0];
66+
callArgs.prv.should.equal(prv);
67+
});
68+
69+
it('should pass wallet instance to baseCoin.signTransaction even when no prv is available', async function () {
70+
sinon.stub(wallet, 'getUserPrv').returns(undefined as any);
71+
const txPrebuild = { txInfo: { payload: '{"amount":"100"}' } } as any;
72+
73+
await wallet.signTransaction({ txPrebuild });
74+
75+
mockBaseCoin.signTransaction.calledOnce.should.be.true();
76+
const callArgs = mockBaseCoin.signTransaction.getCall(0).args[0];
77+
callArgs.wallet.should.equal(wallet);
78+
});
79+
});
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/**
2+
* @prettier
3+
*/
4+
import sinon from 'sinon';
5+
import 'should';
6+
import { Ofc } from '../../../src/coins/ofc';
7+
import { OfcToken } from '../../../src/coins/ofcToken';
8+
import { BaseCoin } from '../../../src/bitgo/baseCoin/baseCoin';
9+
import { Wallet } from '../../../src';
10+
11+
const TEST_TOKEN_CONFIG = {
12+
coin: 'ofcusdt',
13+
decimalPlaces: 6,
14+
name: 'OFCUSDT',
15+
type: 'ofcusdt',
16+
backingCoin: 'usdt',
17+
isFiat: false,
18+
};
19+
20+
describe('Ofc / OfcToken', function () {
21+
let mockBitGo: any;
22+
23+
beforeEach(function () {
24+
mockBitGo = { url: sinon.stub().returns('https://test.bitgo.com') };
25+
});
26+
27+
afterEach(function () {
28+
sinon.restore();
29+
});
30+
31+
describe('signMessage', function () {
32+
let ofc: Ofc;
33+
34+
beforeEach(function () {
35+
ofc = new Ofc(mockBitGo);
36+
});
37+
38+
describe('with a Wallet instance', function () {
39+
it('should delegate to wallet.toTradingAccount().signPayload() and return a Buffer', async function () {
40+
const hexSignature = 'deadbeef';
41+
const signPayloadStub = sinon.stub().resolves(hexSignature);
42+
43+
const mockBaseCoin = { supportsTss: sinon.stub().returns(false), getMPCAlgorithm: sinon.stub() };
44+
const walletData = {
45+
id: 'wallet-id',
46+
keys: ['key1', 'key2', 'key3'],
47+
multisigType: 'onchain',
48+
enterprise: 'ent-id',
49+
};
50+
const wallet = new Wallet(mockBitGo, mockBaseCoin as any, walletData);
51+
sinon.stub(wallet, 'toTradingAccount').returns({ signPayload: signPayloadStub } as any);
52+
53+
const message = 'test message';
54+
const result = await ofc.signMessage(wallet, message);
55+
56+
signPayloadStub.calledOnceWith({ payload: message }).should.be.true();
57+
result.should.deepEqual(Buffer.from(hexSignature, 'hex'));
58+
});
59+
});
60+
61+
describe('with a prv key', function () {
62+
it('should delegate to the base class signMessage', async function () {
63+
const expectedResult = Buffer.from('basesignature', 'hex');
64+
const superSignMessageStub = sinon.stub(BaseCoin.prototype, 'signMessage').resolves(expectedResult);
65+
66+
const key = {
67+
prv: 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqhuCo36EkzGH6qiT9mJHBvuPKtLRYD4NxFb5hgXMQBB2LLT6mxLDHHo',
68+
};
69+
const message = 'test message';
70+
const result = await ofc.signMessage(key, message);
71+
72+
superSignMessageStub.calledOnceWith(key, message).should.be.true();
73+
result.should.equal(expectedResult);
74+
});
75+
});
76+
});
77+
78+
describe('signTransaction (OfcToken)', function () {
79+
let ofcToken: OfcToken;
80+
const payload = '{"amount":"100","from":"alice","to":"bob"}';
81+
82+
beforeEach(function () {
83+
ofcToken = new OfcToken(mockBitGo, TEST_TOKEN_CONFIG);
84+
});
85+
86+
describe('with wallet and no prv (BitGo remote signing)', function () {
87+
it('should call wallet.toTradingAccount().signPayload() without a passphrase', async function () {
88+
const hexSignature = 'aabbccdd';
89+
const signPayloadStub = sinon.stub().resolves(hexSignature);
90+
const mockWallet = { toTradingAccount: sinon.stub().returns({ signPayload: signPayloadStub }) };
91+
92+
const result = await ofcToken.signTransaction({ txPrebuild: { payload }, wallet: mockWallet as any });
93+
94+
signPayloadStub.calledOnceWith({ payload, walletPassphrase: undefined }).should.be.true();
95+
result.should.deepEqual({ halfSigned: { payload, signature: hexSignature } });
96+
});
97+
});
98+
99+
describe('with wallet and prv (local signing routed through wallet)', function () {
100+
it('should call wallet.toTradingAccount().signPayload() with the wallet passphrase', async function () {
101+
const hexSignature = 'aabbccdd';
102+
const passphrase = 'test-passphrase';
103+
const signPayloadStub = sinon.stub().resolves(hexSignature);
104+
const mockWallet = { toTradingAccount: sinon.stub().returns({ signPayload: signPayloadStub }) };
105+
106+
const result = await ofcToken.signTransaction({
107+
txPrebuild: { payload },
108+
wallet: mockWallet as any,
109+
prv: passphrase,
110+
});
111+
112+
signPayloadStub.calledOnceWith({ payload, walletPassphrase: passphrase }).should.be.true();
113+
result.should.deepEqual({ halfSigned: { payload, signature: hexSignature } });
114+
});
115+
});
116+
117+
describe('with prv only (local signing without wallet)', function () {
118+
it('should sign locally and return the correct halfSigned result', async function () {
119+
const signatureBytes = Buffer.from('ccddee', 'hex');
120+
sinon.stub(BaseCoin.prototype, 'signMessage').resolves(signatureBytes);
121+
122+
const result = await ofcToken.signTransaction({ txPrebuild: { payload }, prv: 'test-prv' });
123+
124+
result.should.deepEqual({ halfSigned: { payload, signature: signatureBytes.toString('hex') } });
125+
});
126+
127+
it('should pass the prv to signMessage', async function () {
128+
const signatureBytes = Buffer.from('ccddee', 'hex');
129+
const superSignMessageStub = sinon.stub(BaseCoin.prototype, 'signMessage').resolves(signatureBytes);
130+
const prv = 'test-prv';
131+
132+
await ofcToken.signTransaction({ txPrebuild: { payload }, prv });
133+
134+
superSignMessageStub.calledOnceWith({ prv }, payload).should.be.true();
135+
});
136+
});
137+
138+
describe('with neither wallet nor prv', function () {
139+
it('should throw an error', async function () {
140+
await ofcToken
141+
.signTransaction({ txPrebuild: { payload } })
142+
.should.be.rejectedWith('You must pass in either one of wallet or prv');
143+
});
144+
});
145+
});
146+
});

0 commit comments

Comments
 (0)