Skip to content

Commit f450bda

Browse files
authored
Merge pull request #8803 from BitGo/WCN-217
feat(sdk-core): added OFC BitGo signing on wallet and coins object
2 parents d62e946 + 6c01855 commit f450bda

8 files changed

Lines changed: 417 additions & 71 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export interface SignPayloadParameters {
1818

1919
export interface ITradingAccount {
2020
readonly id: string;
21+
readonly userKeySigningRequired: boolean;
2122
signPayload(params: SignPayloadParameters): Promise<string>;
2223
toNetwork(): ITradingNetwork;
2324
}

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,16 @@ export class TradingAccount implements ITradingAccount {
1111
private readonly enterpriseId: string;
1212

1313
public wallet: IWallet;
14+
public readonly userKeySigningRequired: boolean;
1415

1516
constructor(enterpriseId: string, wallet: IWallet, bitgo: BitGoBase) {
1617
this.enterpriseId = enterpriseId;
1718
this.wallet = wallet;
1819
this.bitgo = bitgo;
20+
21+
const walletData = this.wallet.toJSON();
22+
this.userKeySigningRequired =
23+
walletData.coinSpecific?.userKeySigningRequired ?? walletData.userKeySigningRequired ?? true;
1924
}
2025

2126
get id(): string {
@@ -48,10 +53,9 @@ 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(
54-
'Wallet must use user key to sign ofc transaction, please provide the wallet passphrase or visit your wallet settings page to configure one.'
58+
'Wallet must use user key to sign ofc transaction, please provide the wallet passphrase, the private key, or visit your wallet settings page to configure one.'
5559
);
5660
}
5761
if (walletData.keys.length < 2) {

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: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,25 @@ import {
99
SignTransactionOptions as BaseSignTransactionOptions,
1010
SignedTransaction,
1111
ITransactionRecipient,
12+
IWallet,
1213
} from '../';
1314
import { isBolt11Invoice } from '../lightning';
1415

1516
import { Ofc } from './ofc';
1617

17-
export interface SignTransactionOptions extends BaseSignTransactionOptions {
18-
txPrebuild: {
19-
payload: string;
20-
};
18+
type OfcAllowRemoteSignOptions = BaseSignTransactionOptions & {
19+
txPrebuild: { payload: string };
20+
wallet: IWallet;
21+
prv?: string; // optional: forwarded to signPayload if present
22+
};
23+
24+
type OfcLocalSignOptions = BaseSignTransactionOptions & {
25+
txPrebuild: { payload: string };
2126
prv: string;
22-
}
27+
wallet?: never; // excludes wallet from this branch
28+
};
29+
30+
export type SignTransactionOptions = OfcAllowRemoteSignOptions | OfcLocalSignOptions;
2331

2432
export { OfcTokenConfig };
2533

@@ -107,15 +115,30 @@ export class OfcToken extends Ofc {
107115
}
108116

109117
/**
110-
* Assemble keychain and half-sign prebuilt transaction
118+
* Signs a half-signed OFC transaction.
119+
* Signs the transaction remotely using the BitGo key if prv is not provided.
111120
* @param params
112121
* @returns {Promise<SignedTransaction>}
113122
*/
114123
async signTransaction(params: SignTransactionOptions): Promise<SignedTransaction> {
115124
const txPrebuild = params.txPrebuild;
116125
const payload = txPrebuild.payload;
117-
const signatureBuffer = (await this.signMessage(params, payload)) as any;
118-
const signature: string = signatureBuffer.toString('hex');
126+
127+
let signature: string;
128+
if (params.wallet) {
129+
const tradingAccount = params.wallet.toTradingAccount();
130+
if (!params.prv && tradingAccount.userKeySigningRequired) {
131+
throw new Error(
132+
'Wallet must use user key to sign ofc transaction, please provide the private key or visit your wallet settings page to configure one.'
133+
);
134+
}
135+
signature = await tradingAccount.signPayload({ payload, prv: params.prv });
136+
} else if (params.prv) {
137+
signature = (await this.signMessage({ prv: params.prv }, payload)).toString('hex');
138+
} else {
139+
throw new Error('You must pass in either one of wallet or prv');
140+
}
141+
119142
return { halfSigned: { payload, signature } } as any;
120143
}
121144

modules/sdk-core/test/unit/bitgo/trading/tradingAccount.ts

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import { TradingAccount } from '../../../../src/bitgo/trading/tradingAccount';
77

88
describe('TradingAccount', function () {
99
let tradingAccount: TradingAccount;
10+
let userKeyRequiredTradingAccount: TradingAccount;
1011
let mockBitGo: any;
1112
let mockWallet: any;
13+
let mockUserKeyRequiredWallet: any;
1214
let mockBaseCoin: any;
1315
let sendStub: sinon.SinonStub;
1416

@@ -54,13 +56,27 @@ describe('TradingAccount', function () {
5456
toJSON: sinon.stub().returns({
5557
id: 'test-wallet-id',
5658
keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'],
57-
coinSpecific: {},
59+
coinSpecific: {
60+
userKeySigningRequired: false,
61+
},
5862
}),
5963
baseCoin: mockBaseCoin,
6064
bitgo: mockBitGo,
6165
};
6266

67+
mockUserKeyRequiredWallet = {
68+
...mockWallet,
69+
toJSON: sinon.stub().returns({
70+
id: 'test-wallet-id',
71+
keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'],
72+
coinSpecific: {
73+
userKeySigningRequired: true,
74+
},
75+
}),
76+
};
77+
6378
tradingAccount = new TradingAccount(enterpriseId, mockWallet, mockBitGo);
79+
userKeyRequiredTradingAccount = new TradingAccount(enterpriseId, mockUserKeyRequiredWallet, mockBitGo);
6480
});
6581

6682
afterEach(function () {
@@ -85,7 +101,6 @@ describe('TradingAccount', function () {
85101
it('should sign using the BitGo key remotely when no passphrase is provided', async function () {
86102
const result = await tradingAccount.signPayload({ payload });
87103

88-
mockWallet.toJSON.calledOnce.should.be.true();
89104
mockBitGo.post.calledOnce.should.be.true();
90105
sendStub.calledWith({ payload: JSON.stringify(payload) }).should.be.true();
91106
result.should.equal(signature);
@@ -98,31 +113,18 @@ describe('TradingAccount', function () {
98113
});
99114

100115
it('should throw if coinSpecific.userKeySigningRequired is true and no passphrase and prv are provided', async function () {
101-
mockWallet.toJSON.onCall(mockWallet.toJSON.callCount).returns({
102-
id: 'test-wallet-id',
103-
keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'],
104-
coinSpecific: { userKeySigningRequired: true },
105-
});
106-
107-
await tradingAccount
116+
await userKeyRequiredTradingAccount
108117
.signPayload({ payload })
109118
.should.be.rejectedWith(
110-
'Wallet must use user key to sign ofc transaction, please provide the wallet passphrase or visit your wallet settings page to configure one.'
119+
'Wallet must use user key to sign ofc transaction, please provide the wallet passphrase, the private key, or visit your wallet settings page to configure one.'
111120
);
112121
});
113122

114123
it('should fall back to top-level userKeySigningRequired when coinSpecific does not carry it', async function () {
115-
mockWallet.toJSON.onCall(mockWallet.toJSON.callCount).returns({
116-
id: 'test-wallet-id',
117-
keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'],
118-
coinSpecific: {},
119-
userKeySigningRequired: true,
120-
});
121-
122-
await tradingAccount
124+
await userKeyRequiredTradingAccount
123125
.signPayload({ payload })
124126
.should.be.rejectedWith(
125-
'Wallet must use user key to sign ofc transaction, please provide the wallet passphrase or visit your wallet settings page to configure one.'
127+
'Wallet must use user key to sign ofc transaction, please provide the wallet passphrase, the private key, or visit your wallet settings page to configure one.'
126128
);
127129
});
128130

0 commit comments

Comments
 (0)