Skip to content

Commit 16bcfef

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 b6be824 commit 16bcfef

6 files changed

Lines changed: 311 additions & 47 deletions

File tree

modules/sdk-core/src/bitgo/trading/tradingAccount.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ export class TradingAccount implements ITradingAccount {
2222
return this.wallet.id();
2323
}
2424

25+
get userKeySigningRequired(): boolean {
26+
const walletData = this.wallet.toJSON();
27+
return walletData.coinSpecific?.userKeySigningRequired ?? walletData.userKeySigningRequired ?? true;
28+
}
29+
2530
/**
2631
* Signs an arbitrary payload. Use the user key if passphrase/prv is provided, or the BitGo key if not.
2732
* @param params
@@ -48,8 +53,7 @@ export class TradingAccount implements ITradingAccount {
4853
params: Omit<SignPayloadParameters, 'walletPassphrase' | 'prv'>
4954
): Promise<string> {
5055
const walletData = this.wallet.toJSON();
51-
const userKeySigningRequired = walletData.coinSpecific?.userKeySigningRequired ?? walletData.userKeySigningRequired;
52-
if (userKeySigningRequired) {
56+
if (this.userKeySigningRequired) {
5357
throw new Error(
5458
'Wallet must use user key to sign ofc transaction, please provide the wallet passphrase or visit your wallet settings page to configure one.'
5559
);

modules/sdk-core/src/bitgo/wallet/wallet.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2137,7 +2137,7 @@ export class Wallet implements IWallet {
21372137
* @param params
21382138
* - txPrebuild
21392139
* - [keychain / key] (object) or prv (string)
2140-
* - walletPassphrase
2140+
* - walletPassphrase (optional ONLY for OFC wallets with userKeySigningRequired = false)
21412141
* - verifyTxParams (optional) - when provided, the transaction will be verified before signing
21422142
* - txParams: transaction parameters used for verification
21432143
* - verification: optional verification options
@@ -2264,6 +2264,17 @@ export class Wallet implements IWallet {
22642264
};
22652265
return params.customSigningFunction(signTransactionParamsWithSeed);
22662266
}
2267+
2268+
if (this.baseCoin.getFamily() === 'ofc') {
2269+
const userKeySigningRequired = this.toTradingAccount().userKeySigningRequired;
2270+
const prv = userKeySigningRequired ? await this.getUserPrvAsync(presign as GetUserPrvOptions) : undefined;
2271+
return this.baseCoin.signTransaction({
2272+
...signTransactionParams,
2273+
prv,
2274+
wallet: this,
2275+
});
2276+
}
2277+
22672278
return this.baseCoin.signTransaction({
22682279
...signTransactionParams,
22692280
prv: await this.getUserPrvAsync(presign as GetUserPrvOptions),

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, message);
123+
}
124+
const signatureHexString = await keyOrWallet.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, prv: 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

modules/sdk-core/test/unit/bitgo/wallet/ofcWalletSignTransaction.ts

Lines changed: 119 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,66 +3,145 @@
33
*/
44
import sinon from 'sinon';
55
import 'should';
6-
import { Wallet } from '../../../../src';
6+
import { Wallet, BaseCoin } from '../../../../src';
7+
import { OfcToken } from '../../../../src/coins';
78

8-
describe('Wallet - OFC signTransaction', function () {
9+
const TEST_TOKEN_CONFIG = {
10+
coin: 'ofcusdt',
11+
decimalPlaces: 6,
12+
name: 'OFCUSDT',
13+
type: 'ofcusdt',
14+
backingCoin: 'usdt',
15+
isFiat: false,
16+
};
17+
18+
describe('Wallet - OFC', function () {
919
let wallet: Wallet;
1020
let mockBitGo: any;
11-
let mockBaseCoin: any;
12-
let mockWalletData: any;
21+
let ofcToken: OfcToken;
22+
let signMessageStub: sinon.SinonStub;
23+
const signatureBytes = Buffer.from('aabbccdd', 'hex');
24+
const payload = '{"amount":"100","from":"alice","to":"bob"}';
25+
const prv = 'test-prv';
26+
// bg-<32 hex chars> is valid for OfcToken without needing to resolve the backing coin
27+
const recipients = [{ address: 'bg-aabbccddeeff00112233445566778899', amount: '100' }];
28+
const userPub =
29+
'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC51xERxh8vDMjc3PiGpQ3VeQcHBiGxbsrRXbCKQZZxBtDMrmBRzxRMkBJGsC9bsYb5tggVF3jGfEE';
30+
31+
const walletData = {
32+
id: 'test-wallet-id',
33+
coin: 'ofcusdt',
34+
keys: ['user-key', 'backup-key', 'bitgo-key'],
35+
type: 'trading',
36+
multisigType: 'onchain',
37+
enterprise: 'ent-id',
38+
};
39+
40+
/**
41+
* Creates a fluent request mock that handles .query().send().result() and .send().result()
42+
* chains, resolving with the provided value.
43+
*/
44+
function makeRequestChain(resolved: unknown): any {
45+
const chain: any = {};
46+
chain.query = sinon.stub().returns(chain);
47+
chain.send = sinon.stub().returns(chain);
48+
chain.result = sinon.stub().resolves(resolved);
49+
return chain;
50+
}
1351

1452
beforeEach(function () {
53+
signMessageStub = sinon.stub(BaseCoin.prototype, 'signMessage');
54+
signMessageStub.withArgs({ prv }, payload).resolves(signatureBytes);
55+
sinon.stub(BaseCoin.prototype, 'keychains').returns({
56+
getKeysForSigning: sinon.stub().resolves([{ id: walletData.keys[0], pub: userPub }]),
57+
} as any);
1558
mockBitGo = {
16-
url: sinon.stub().returns('https://test.bitgo.com'),
59+
url: sinon.stub().returns('https://test.bitgo.com/'),
1760
post: sinon.stub(),
18-
get: sinon.stub(),
1961
setRequestTracer: sinon.stub(),
2062
};
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);
63+
ofcToken = new OfcToken(mockBitGo, TEST_TOKEN_CONFIG);
64+
wallet = new Wallet(mockBitGo, ofcToken, walletData);
4265
});
4366

4467
afterEach(function () {
4568
sinon.restore();
4669
});
4770

48-
it('should pass prv and paylaod to baseCoin.signTransaction', async function () {
49-
const txPrebuild = { txInfo: { payload: '{"amount":"100"}' } } as any;
50-
const prv = 'test-prv';
71+
describe('signTransaction', function () {
72+
describe('with prv (local signing routed through wallet)', function () {
73+
it('should call baseCoin.signMessage with the prv and payload', async function () {
74+
const result = await wallet.signTransaction({ txPrebuild: { payload } as any, prv });
75+
76+
signMessageStub.calledOnceWith({ prv }, payload).should.be.true();
77+
mockBitGo.post.called.should.be.false();
78+
result.should.deepEqual({ halfSigned: { payload, signature: signatureBytes.toString('hex') } });
79+
});
80+
});
81+
82+
describe('with walletPassphrase and keychain', function () {
83+
const walletPassphrase = 'test-passphrase';
84+
const encryptedPrv = 'encrypted-prv-blob';
5185

52-
await wallet.signTransaction({ txPrebuild, prv });
86+
beforeEach(function () {
87+
// getUserPrvAsync decrypts the keychain via the compiled decryptKeychainPrivateKey,
88+
// which calls bitgo.decrypt synchronously
89+
mockBitGo.decrypt = sinon.stub().returns(prv);
90+
});
5391

54-
mockBaseCoin.signTransaction.calledOnce.should.be.true();
55-
mockBaseCoin.signTransaction.calledWith({ txPrebuild, prv });
56-
const callArgs = mockBaseCoin.signTransaction.getCall(0).args[0];
57-
callArgs.prv.should.equal(prv);
92+
it('should decrypt the keychain with the passphrase and sign via baseCoin.signMessage', async function () {
93+
const result = await wallet.signTransaction({
94+
txPrebuild: { payload } as any,
95+
walletPassphrase,
96+
keychain: { pub: userPub, encryptedPrv } as any,
97+
});
98+
99+
signMessageStub.calledOnceWith({ prv }, payload).should.be.true();
100+
mockBitGo.post.called.should.be.false();
101+
result.should.deepEqual({ halfSigned: { payload, signature: signatureBytes.toString('hex') } });
102+
});
103+
});
58104
});
59105

60-
it('should return the result from baseCoin.signTransaction', async function () {
61-
const txPrebuild = { txInfo: { payload: '{"amount":"100"}' } } as any;
62-
const prv = 'test-prv';
106+
describe('prebuildAndSignTransaction', function () {
107+
const buildUrl = () => ofcToken.url('/wallet/' + walletData.id + '/tx/build');
108+
const signUrl = () => ofcToken.url('/wallet/' + walletData.id + '/tx/sign');
109+
110+
beforeEach(function () {
111+
mockBitGo.post.withArgs(buildUrl()).returns(makeRequestChain({ payload }));
112+
});
113+
114+
it('should call the prebuild API then sign locally with the prv', async function () {
115+
const result = await wallet.prebuildAndSignTransaction({ prv });
116+
117+
mockBitGo.post.calledWith(buildUrl()).should.be.true();
118+
signMessageStub.calledOnceWith({ prv }, payload).should.be.true();
119+
result.should.deepEqual({ halfSigned: { payload, signature: signatureBytes.toString('hex') } });
120+
});
121+
122+
it('should not call the remote sign endpoint when prv is provided', async function () {
123+
await wallet.prebuildAndSignTransaction({ prv });
124+
125+
mockBitGo.post.calledWith(signUrl()).should.be.false();
126+
});
127+
});
128+
129+
describe('sendMany', function () {
130+
const buildUrl = () => ofcToken.url('/wallet/' + walletData.id + '/tx/build');
131+
const sendUrl = () => ofcToken.url('/wallet/' + walletData.id + '/tx/send');
132+
133+
beforeEach(function () {
134+
mockBitGo.post.withArgs(buildUrl()).returns(makeRequestChain({ payload }));
135+
mockBitGo.post.withArgs(sendUrl()).returns(makeRequestChain({ txid: 'test-txid', status: 'signed' }));
136+
});
63137

64-
const result = await wallet.signTransaction({ txPrebuild, prv });
138+
it('should prebuild, sign locally, and submit to the send endpoint', async function () {
139+
const result = await wallet.sendMany({ recipients, prv });
65140

66-
result.should.deepEqual({ halfSigned: { payload: 'test', signature: 'aabbcc' } });
141+
mockBitGo.post.calledWith(buildUrl()).should.be.true();
142+
signMessageStub.calledOnceWith({ prv }, payload).should.be.true();
143+
mockBitGo.post.calledWith(sendUrl()).should.be.true();
144+
result.should.deepEqual({ txid: 'test-txid', status: 'signed' });
145+
});
67146
});
68147
});

0 commit comments

Comments
 (0)