Skip to content

Commit c3aff4f

Browse files
fix(sdk-core): skip keychain fetch in createAddress for OFC wallets
OFC coins never use keychains for address verification — isWalletAddress always throws MethodNotImplementedError and the check is skipped. Fetching all wallet keys was unnecessary and failed for wallets where a server-managed key at index 1 has no accessible keychain record in the OFC namespace. Fixes WCN-942 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 0ec7f61 commit c3aff4f

2 files changed

Lines changed: 160 additions & 2 deletions

File tree

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1462,8 +1462,12 @@ export class Wallet implements IWallet {
14621462
addressParams.evmKeyRingReferenceAddress = evmKeyRingReferenceAddress;
14631463
}
14641464

1465-
// get keychains for address verification
1466-
const keychains = await Promise.all(this._wallet.keys.map((k) => this.baseCoin.keychains().get({ id: k, reqId })));
1465+
// OFC coins skip address verification (isWalletAddress throws MethodNotImplementedError),
1466+
// so fetching keychains is unnecessary and can fail for server-managed keys.
1467+
const keychains =
1468+
this.baseCoin.getFamily() === 'ofc'
1469+
? []
1470+
: await Promise.all(this._wallet.keys.map((k) => this.baseCoin.keychains().get({ id: k, reqId })));
14671471
const rootAddress = _.get(this._wallet, 'receiveAddress.address');
14681472

14691473
const newAddresses = _.times(count, async () => {
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/**
2+
* @prettier
3+
*/
4+
import sinon from 'sinon';
5+
import 'should';
6+
import { Wallet } from '../../../../src/bitgo/wallet/wallet';
7+
import { OfcToken } from '../../../../src/coins';
8+
9+
const OFC_ZEC_CONFIG = {
10+
coin: 'ofczec',
11+
decimalPlaces: 8,
12+
name: 'OFCZEC',
13+
type: 'ofczec',
14+
backingCoin: 'zec',
15+
isFiat: false,
16+
};
17+
18+
describe('Wallet - OFC createAddress', function () {
19+
let mockBitGo: any;
20+
let ofcToken: OfcToken;
21+
let keychainsGetStub: sinon.SinonStub;
22+
23+
function makePostChain(resolved: unknown): any {
24+
const chain: any = {};
25+
chain.send = sinon.stub().returns(chain);
26+
chain.result = sinon.stub().resolves(resolved);
27+
return chain;
28+
}
29+
30+
beforeEach(function () {
31+
keychainsGetStub = sinon.stub().resolves({ id: 'user-key-id', pub: 'xpub-value' });
32+
mockBitGo = {
33+
url: sinon.stub().returns('https://test.bitgo.com/'),
34+
post: sinon.stub(),
35+
setRequestTracer: sinon.stub(),
36+
};
37+
ofcToken = new OfcToken(mockBitGo, OFC_ZEC_CONFIG);
38+
mockBitGo.coin = sinon.stub().returns(ofcToken);
39+
sinon.stub(OfcToken.prototype, 'keychains').returns({ get: keychainsGetStub } as any);
40+
});
41+
42+
afterEach(function () {
43+
sinon.restore();
44+
});
45+
46+
const mockAddressResponse = {
47+
id: 'new-address-id',
48+
address: 'bg-aabbccddeeff00112233445566778899',
49+
chain: 0,
50+
index: 1,
51+
};
52+
53+
describe('single-key OFC wallet (userKeySigningRequired: true)', function () {
54+
it('should create a receive address without fetching any keychains', async function () {
55+
const walletData = {
56+
id: 'wallet-id',
57+
coin: 'ofc',
58+
keys: ['user-key-id'],
59+
type: 'trading',
60+
multisigType: 'onchain',
61+
enterprise: 'ent-id',
62+
userKeySigningRequired: true,
63+
};
64+
const wallet = new Wallet(mockBitGo, ofcToken, walletData);
65+
mockBitGo.post.returns(makePostChain(mockAddressResponse));
66+
67+
const result = await wallet.createAddress({ onToken: 'ofczec' });
68+
69+
result.should.have.property('id', 'new-address-id');
70+
result.should.have.property('address', 'bg-aabbccddeeff00112233445566778899');
71+
keychainsGetStub.called.should.be.false();
72+
});
73+
});
74+
75+
describe('two-key OFC wallet (userKeySigningRequired: false)', function () {
76+
it('should create a receive address without fetching keychains', async function () {
77+
const walletData = {
78+
id: 'wallet-id',
79+
coin: 'ofc',
80+
keys: ['user-key-id', 'bitgo-key-id'],
81+
type: 'trading',
82+
multisigType: 'onchain',
83+
enterprise: 'ent-id',
84+
userKeySigningRequired: false,
85+
};
86+
const wallet = new Wallet(mockBitGo, ofcToken, walletData);
87+
mockBitGo.post.returns(makePostChain(mockAddressResponse));
88+
89+
const result = await wallet.createAddress({ onToken: 'ofczec' });
90+
91+
result.should.have.property('id', 'new-address-id');
92+
keychainsGetStub.called.should.be.false();
93+
});
94+
95+
it('should succeed even if keychains().get() would fail for a server-managed key', async function () {
96+
keychainsGetStub.rejects(new Error('key not found'));
97+
const walletData = {
98+
id: 'wallet-id',
99+
coin: 'ofc',
100+
keys: ['user-key-id', 'bitgo-key-id'],
101+
type: 'trading',
102+
multisigType: 'onchain',
103+
enterprise: 'ent-id',
104+
userKeySigningRequired: false,
105+
};
106+
const wallet = new Wallet(mockBitGo, ofcToken, walletData);
107+
mockBitGo.post.returns(makePostChain(mockAddressResponse));
108+
109+
const result = await wallet.createAddress({ onToken: 'ofczec' });
110+
result.should.have.property('id', 'new-address-id');
111+
});
112+
});
113+
114+
describe('non-OFC wallet (eth)', function () {
115+
it('should still fetch keychains for address verification', async function () {
116+
const mockEthCoin: any = {
117+
getFamily: sinon.stub().returns('eth'),
118+
isEVM: sinon.stub().returns(true),
119+
supportsTss: sinon.stub().returns(false),
120+
url: sinon.stub().returns('/api/v2/eth/wallet/wallet-id/address'),
121+
isWalletAddress: sinon.stub().resolves(true),
122+
keychains: sinon.stub().returns({ get: keychainsGetStub }),
123+
};
124+
const walletData = {
125+
id: 'wallet-id',
126+
coin: 'eth',
127+
keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'],
128+
coinSpecific: { pendingChainInitialization: false },
129+
};
130+
const wallet = new Wallet(mockBitGo, mockEthCoin, walletData);
131+
mockBitGo.post.returns(makePostChain({ id: 'eth-addr-id', address: '0xabc', coinSpecific: {} }));
132+
133+
await wallet.createAddress({});
134+
135+
keychainsGetStub.callCount.should.equal(3);
136+
});
137+
});
138+
139+
describe('missing onToken parameter', function () {
140+
it('should throw for OFC wallets when onToken is omitted', async function () {
141+
const walletData = {
142+
id: 'wallet-id',
143+
coin: 'ofc',
144+
keys: ['user-key-id'],
145+
type: 'trading',
146+
multisigType: 'onchain',
147+
enterprise: 'ent-id',
148+
};
149+
const wallet = new Wallet(mockBitGo, ofcToken, walletData);
150+
151+
await wallet.createAddress({}).should.be.rejectedWith('onToken is a mandatory parameter for OFC wallets');
152+
});
153+
});
154+
});

0 commit comments

Comments
 (0)