From a828082a56e0b6bc32224dfadc399639c40d6314 Mon Sep 17 00:00:00 2001 From: Daniel Peng Date: Fri, 21 Aug 2026 14:16:41 -0400 Subject: [PATCH] fix: read wallet.safe from WP and resolve mint slot TICKET: WCN-1203 --- modules/sdk-core/src/bitgo/safe/safe.ts | 4 ++-- .../sdk-core/src/bitgo/safe/safeDerivation.ts | 2 +- modules/sdk-core/src/bitgo/wallet/iWallet.ts | 5 +++-- modules/sdk-core/src/bitgo/wallet/wallet.ts | 6 ++++- modules/sdk-core/test/unit/bitgo/safe/safe.ts | 13 +++++------ .../test/unit/bitgo/wallet/safeGetUserPrv.ts | 22 +++++++++---------- 6 files changed, 28 insertions(+), 24 deletions(-) diff --git a/modules/sdk-core/src/bitgo/safe/safe.ts b/modules/sdk-core/src/bitgo/safe/safe.ts index 3d2a2271ce..eb2f038b9c 100644 --- a/modules/sdk-core/src/bitgo/safe/safe.ts +++ b/modules/sdk-core/src/bitgo/safe/safe.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; import { FreezeSafeBody, SafeData, SafeShareData, SafeShareState, type RootKeyType } from '@bitgo/public-types'; -import { KeyCurve } from '@bitgo/statics'; +import { coins, KeyCurve } from '@bitgo/statics'; import { IBaseCoin } from '../baseCoin'; import { BitGoBase } from '../bitgoBase'; import { IncorrectPasswordError } from '../errors'; @@ -49,7 +49,7 @@ function onchainSlotForCoin(coin: IBaseCoin): Extract'` from the sequential `safe.derivationIndex[slot]`. * Backup / BitGo children are soft-derived server-side at `m/` (not here). diff --git a/modules/sdk-core/src/bitgo/wallet/iWallet.ts b/modules/sdk-core/src/bitgo/wallet/iWallet.ts index 3fb40b46b3..06fca6da8a 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallet.ts @@ -1016,8 +1016,8 @@ export interface WalletData { evmKeyRingReferenceWalletId?: string; isParent?: boolean; enabledChildChains?: string[]; - /** Set on child wallets that belong to a safe. */ - safeId?: string; + /** @experimental Public id of the parent safe this wallet was minted in */ + safe?: string; /** * @deprecated Read from `coinSpecific.userKeySigningRequired` instead. Retained * temporarily as a fallback while the field migrates from the top level to the OFC @@ -1187,6 +1187,7 @@ export interface IWallet { subType(): SubWalletType | undefined; multisigType(): 'onchain' | 'tss'; multisigTypeVersion(): 'MPCv2' | undefined; + /** @experimental Public id of the parent safe this wallet was minted in */ safeId(): string | undefined; label(): string; keyIds(): string[]; diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index 30b1bd4f3d..4618486c50 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -385,8 +385,12 @@ export class Wallet implements IWallet { return this._wallet.multisigTypeVersion; } + /** + * Public id of the parent safe, if this wallet was minted in one. + * @experimental + */ safeId(): string | undefined { - return this._wallet.safeId; + return this._wallet.safe; } subType(): SubWalletType | undefined { diff --git a/modules/sdk-core/test/unit/bitgo/safe/safe.ts b/modules/sdk-core/test/unit/bitgo/safe/safe.ts index b4122eb095..898f23f6e8 100644 --- a/modules/sdk-core/test/unit/bitgo/safe/safe.ts +++ b/modules/sdk-core/test/unit/bitgo/safe/safe.ts @@ -133,7 +133,7 @@ describe('Safe', function () { let derivationQuery: sinon.SinonStub; let mintSend: sinon.SinonStub; - function stubCoin(primaryKeyCurve: string, opts: { getDefaultMultisigType?: string } = {}) { + function stubCoin(chain: string, opts: { getDefaultMultisigType?: string } = {}) { keychainsGet = sinon.stub().resolves({ id: 'user-root-id', source: 'user', @@ -143,8 +143,7 @@ describe('Safe', function () { }); keychainsAdd = sinon.stub().resolves({ id: 'child-key-id', pub: childAt0.pub, type: 'independent' }); mockBitGo.coin = sinon.stub().returns({ - getChain: sinon.stub().returns('tbtc'), - getConfig: sinon.stub().returns({ primaryKeyCurve }), + getChain: sinon.stub().returns(chain), getDefaultMultisigType: sinon.stub().returns(opts.getDefaultMultisigType), supportsTss: sinon.stub().returns(false), keychains: sinon.stub().returns({ get: keychainsGet, add: keychainsAdd }), @@ -152,7 +151,7 @@ describe('Safe', function () { } beforeEach(function () { - stubCoin('secp256k1'); + stubCoin('tbtc'); mockBitGo.decrypt = sinon.stub().callsFake(({ input, password }: { input: string; password: string }) => { if (password !== 'pw') { throw new Error('bad password'); @@ -173,7 +172,7 @@ describe('Safe', function () { type: 'hot', multisigType: 'onchain', enterprise: 'test-enterprise-id', - safeId: 'test-safe-id', + safe: 'test-safe-id', }); mintSend = sinon.stub().returns({ result: mintResult }); mockBitGo.post.returns({ send: mintSend }); @@ -219,7 +218,7 @@ describe('Safe', function () { }); it('rejects a TSS-default coin even without multisigType tss', async function () { - stubCoin('secp256k1', { getDefaultMultisigType: 'tss' }); + stubCoin('hteth', { getDefaultMultisigType: 'tss' }); await safe .createWallet({ coin: 'hteth', label: 'evm', passphrase: 'pw' }) .should.be.rejectedWith(/MPC safe wallet minting is not yet implemented/); @@ -235,7 +234,7 @@ describe('Safe', function () { }); it('rejects ed25519 onchain coins', async function () { - stubCoin('ed25519'); + stubCoin('txlm'); await safe .createWallet({ coin: 'txlm', label: 'xlm', passphrase: 'pw' }) .should.be.rejectedWith(/ed25519 coin safe wallet minting is not yet supported/); diff --git a/modules/sdk-core/test/unit/bitgo/wallet/safeGetUserPrv.ts b/modules/sdk-core/test/unit/bitgo/wallet/safeGetUserPrv.ts index 57a2ef83bf..19c859f903 100644 --- a/modules/sdk-core/test/unit/bitgo/wallet/safeGetUserPrv.ts +++ b/modules/sdk-core/test/unit/bitgo/wallet/safeGetUserPrv.ts @@ -213,7 +213,7 @@ describe('WCN-1200 safe child getUserPrv root-fetch detour', function () { }); it('throws for a safe wallet when keychain has no parent', async function () { - const wallet = makeWallet({ safeId: 'safe-id-1' }); + const wallet = makeWallet({ safe: 'safe-id-1' }); await wallet .getUserPrv({ keychain: { @@ -228,7 +228,7 @@ describe('WCN-1200 safe child getUserPrv root-fetch detour', function () { }); it('fetches root and hardened-derives child key for safe owner', async function () { - const wallet = makeWallet({ safeId: 'safe-id-1' }); + const wallet = makeWallet({ safe: 'safe-id-1' }); keychainsGetStub.resolves({ id: rootKeyId, source: 'user', @@ -254,7 +254,7 @@ describe('WCN-1200 safe child getUserPrv root-fetch detour', function () { }); it('fails closed for TSS safe owner instead of returning the root prv', async function () { - const wallet = makeWallet({ safeId: 'safe-id-1', multisigType: 'tss' }); + const wallet = makeWallet({ safe: 'safe-id-1', multisigType: 'tss' }); await wallet .getUserPrv({ @@ -275,7 +275,7 @@ describe('WCN-1200 safe child getUserPrv root-fetch detour', function () { it('fails closed for ed25519 onchain safe owner instead of BIP32-deriving', async function () { mockBaseCoin.getFamily.returns('xlm'); - const wallet = makeWallet({ safeId: 'safe-id-1', coin: 'txlm' }); + const wallet = makeWallet({ safe: 'safe-id-1', coin: 'txlm' }); await wallet .getUserPrv({ @@ -294,7 +294,7 @@ describe('WCN-1200 safe child getUserPrv root-fetch detour', function () { }); it('aborts locally when derived pub does not match registered child pub', async function () { - const wallet = makeWallet({ safeId: 'safe-id-1' }); + const wallet = makeWallet({ safe: 'safe-id-1' }); keychainsGetStub.resolves({ id: rootKeyId, source: 'user', @@ -319,7 +319,7 @@ describe('WCN-1200 safe child getUserPrv root-fetch detour', function () { it('decrypts child encryptedPrv as-is for wallet sharee (hardened child prv)', async function () { const childPrv = 'child-level-prv'; - const wallet = makeWallet({ safeId: 'safe-id-1' }); + const wallet = makeWallet({ safe: 'safe-id-1' }); const result = await wallet.getUserPrv({ keychain: { @@ -340,7 +340,7 @@ describe('WCN-1200 safe child getUserPrv root-fetch detour', function () { it('does not auto-populate coldDerivationSeed when explicit prv and encryptedPrv are present', async function () { const childPrv = 'child-level-prv'; - const wallet = makeWallet({ safeId: 'safe-id-1' }); + const wallet = makeWallet({ safe: 'safe-id-1' }); const result = await wallet.getUserPrv({ prv: childPrv, @@ -410,7 +410,7 @@ describe('WCN-1200 safe child getUserPrv root-fetch detour', function () { describe('getEncryptedUserKeychain', function () { it('still fails for a safe owner so wallet sharing cannot obtain root material', async function () { - const wallet = makeWallet({ safeId: 'safe-id-1' }); + const wallet = makeWallet({ safe: 'safe-id-1' }); keychainsGetStub.resolves({ id: 'user-key', pub: 'child-pub', @@ -427,7 +427,7 @@ describe('WCN-1200 safe child getUserPrv root-fetch detour', function () { describe('signing guards', function () { it('getUserKeyAndSignTssTransaction allows safe child keychain without encryptedPrv', async function () { const wallet = makeWallet({ - safeId: 'safe-id-1', + safe: 'safe-id-1', multisigType: 'tss', type: 'hot', }); @@ -464,7 +464,7 @@ describe('WCN-1200 safe child getUserPrv root-fetch detour', function () { it('getUserKeyAndSignTssTransaction rejects wrong passphrase early for safe child wallets', async function () { const wallet = makeWallet({ - safeId: 'safe-id-1', + safe: 'safe-id-1', multisigType: 'tss', type: 'hot', }); @@ -501,7 +501,7 @@ describe('WCN-1200 safe child getUserPrv root-fetch detour', function () { it('signTransaction does not pass the root prv into TSS signing for a safe owner', async function () { const wallet = makeWallet({ - safeId: 'safe-id-1', + safe: 'safe-id-1', multisigType: 'tss', type: 'hot', });