Skip to content

Commit a09e279

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 a09e279

3 files changed

Lines changed: 190 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, 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, 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: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
/**
2+
* @prettier
3+
*/
4+
import sinon from 'sinon';
5+
import 'should';
6+
import { Ofc, OfcToken } from '../../../src/coins';
7+
import { BaseCoin, Wallet } from '../../../src';
8+
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('Ofc / OfcToken', function () {
19+
let mockBitGo: any;
20+
let coinUrlStub: sinon.SinonStub;
21+
const hexSignature = 'deadbeef';
22+
const signUrl = 'https://test.bitgo.com/api/v2/ofc/wallet/wallet-id/tx/sign';
23+
const walletData = {
24+
id: 'wallet-id',
25+
keys: ['userKey', 'bitgoKey'],
26+
type: 'trading',
27+
multisigType: 'onchain',
28+
enterprise: 'ent-id',
29+
};
30+
31+
beforeEach(function () {
32+
coinUrlStub = sinon.stub().returns(signUrl);
33+
mockBitGo = {
34+
url: sinon.stub().returns('https://test.bitgo.com/'),
35+
post: sinon.stub().returnsThis(),
36+
send: sinon.stub().returnsThis(),
37+
result: sinon.stub().resolves({ signature: hexSignature }),
38+
coin: sinon.stub().returns({ url: coinUrlStub }),
39+
};
40+
});
41+
42+
afterEach(function () {
43+
sinon.restore();
44+
});
45+
46+
describe('signMessage', function () {
47+
let ofc: Ofc;
48+
49+
beforeEach(function () {
50+
ofc = new Ofc(mockBitGo);
51+
});
52+
53+
describe('with a Wallet instance', function () {
54+
it('should delegate to wallet.toTradingAccount().signPayload() and return a Buffer', async function () {
55+
const wallet = new Wallet(mockBitGo, ofc, walletData);
56+
57+
const message = 'test message';
58+
const result = await ofc.signMessage(wallet, message);
59+
60+
mockBitGo.send.calledOnceWith({ payload: message }).should.be.true();
61+
mockBitGo.result.calledOnce.should.be.true();
62+
result.should.deepEqual(Buffer.from(hexSignature, 'hex'));
63+
});
64+
});
65+
66+
describe('with a prv key', function () {
67+
it('should delegate to the base class signMessage with the exact key and message', async function () {
68+
const expectedResult = Buffer.from('basesignature', 'hex');
69+
const superSignMessageStub = sinon.stub(BaseCoin.prototype, 'signMessage').resolves(expectedResult);
70+
71+
const key = {
72+
prv: 'xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqhuCo36EkzGH6qiT9mJHBvuPKtLRYD4NxFb5hgXMQBB2LLT6mxLDHHo',
73+
};
74+
const message = 'test message';
75+
const result = await ofc.signMessage(key, message);
76+
77+
superSignMessageStub.calledOnceWith(key, message).should.be.true();
78+
result.should.equal(expectedResult);
79+
});
80+
});
81+
});
82+
83+
describe('signTransaction (OfcToken)', function () {
84+
let ofcToken: OfcToken;
85+
const payload = '{"amount":"100","from":"alice","to":"bob"}';
86+
87+
beforeEach(function () {
88+
ofcToken = new OfcToken(mockBitGo, TEST_TOKEN_CONFIG);
89+
});
90+
91+
describe('with wallet and no prv (BitGo remote signing)', function () {
92+
it('should POST to the sign endpoint with the payload and return the halfSigned result', async function () {
93+
const wallet = new Wallet(mockBitGo, ofcToken, walletData);
94+
const result = await ofcToken.signTransaction({ txPrebuild: { payload }, wallet });
95+
96+
mockBitGo.coin.calledOnceWith('ofc').should.be.true();
97+
coinUrlStub.calledOnceWith('/wallet/wallet-id/tx/sign').should.be.true();
98+
mockBitGo.post.calledOnceWith(signUrl).should.be.true();
99+
mockBitGo.send.calledOnceWith({ payload }).should.be.true();
100+
result.should.deepEqual({ halfSigned: { payload, signature: hexSignature } });
101+
});
102+
});
103+
104+
describe('with wallet and prv (local signing routed through wallet)', function () {
105+
it('should fetch and decrypt the user key, then sign via baseCoin.signMessage', async function () {
106+
const passphrase = 'test-passphrase';
107+
const encryptedPrv = 'encrypted-prv-value';
108+
const decryptedPrv = 'decrypted-prv-value';
109+
const signatureBytes = Buffer.from('aabbccdd', 'hex');
110+
111+
const keychainsGetStub = sinon.stub().resolves({ encryptedPrv });
112+
sinon.stub(ofcToken as any, 'keychains').returns({ get: keychainsGetStub });
113+
mockBitGo.decrypt = sinon.stub().returns(decryptedPrv);
114+
const signMessageStub = sinon.stub(BaseCoin.prototype, 'signMessage').resolves(signatureBytes);
115+
116+
const wallet = new Wallet(mockBitGo, ofcToken, walletData);
117+
const result = await ofcToken.signTransaction({
118+
txPrebuild: { payload },
119+
wallet,
120+
prv: passphrase,
121+
});
122+
123+
keychainsGetStub.calledOnceWith({ id: walletData.keys[0] }).should.be.true();
124+
(mockBitGo.decrypt as sinon.SinonStub)
125+
.calledOnceWith({ input: encryptedPrv, password: passphrase })
126+
.should.be.true();
127+
signMessageStub.calledOnceWith({ prv: decryptedPrv }, payload).should.be.true();
128+
result.should.deepEqual({ halfSigned: { payload, signature: signatureBytes.toString('hex') } });
129+
});
130+
});
131+
132+
describe('with prv only (local signing without wallet)', function () {
133+
it('should pass the prv to baseCoin.signMessage and return the correct halfSigned result', async function () {
134+
const prv = 'test-prv';
135+
const signatureBytes = Buffer.from('ccddee', 'hex');
136+
const superSignMessageStub = sinon.stub(BaseCoin.prototype, 'signMessage').resolves(signatureBytes);
137+
138+
const result = await ofcToken.signTransaction({ txPrebuild: { payload }, prv });
139+
140+
superSignMessageStub.calledOnceWith({ prv }, payload).should.be.true();
141+
result.should.deepEqual({ halfSigned: { payload, signature: signatureBytes.toString('hex') } });
142+
});
143+
});
144+
145+
describe('with neither wallet nor prv', function () {
146+
it('should throw an error', async function () {
147+
await ofcToken
148+
.signTransaction({ txPrebuild: { payload } })
149+
.should.be.rejectedWith('You must pass in either one of wallet or prv');
150+
});
151+
});
152+
});
153+
});

0 commit comments

Comments
 (0)