From ce61ce5ab84bb3ab47ea8ca205ae8ae60f20e20b Mon Sep 17 00:00:00 2001 From: Daniel Peng Date: Thu, 20 Aug 2026 11:38:28 -0400 Subject: [PATCH] feat: mint safe wallets via hardened user-child derivation Ticket: WCN-1203 --- .../sdk-core/src/bitgo/keychain/iKeychains.ts | 2 + .../sdk-core/src/bitgo/keychain/keychains.ts | 2 + modules/sdk-core/src/bitgo/safe/iSafe.ts | 8 +- modules/sdk-core/src/bitgo/safe/safe.ts | 122 +++++++++++++- .../sdk-core/src/bitgo/safe/safeDerivation.ts | 74 +++++++-- .../sdk-core/src/bitgo/wallet/safeKeychain.ts | 23 ++- modules/sdk-core/test/unit/bitgo/safe/safe.ts | 155 +++++++++++++++++- .../test/unit/bitgo/wallet/safeGetUserPrv.ts | 40 +---- 8 files changed, 350 insertions(+), 76 deletions(-) diff --git a/modules/sdk-core/src/bitgo/keychain/iKeychains.ts b/modules/sdk-core/src/bitgo/keychain/iKeychains.ts index 56da033ec9..1601b94eb4 100644 --- a/modules/sdk-core/src/bitgo/keychain/iKeychains.ts +++ b/modules/sdk-core/src/bitgo/keychain/iKeychains.ts @@ -146,6 +146,8 @@ export interface AddKeychainOptions { originalPasscodeEncryptionCode?: string; enterprise?: string; derivedFromParentWithSeed?: any; + /** Safe user-root key id this child was derived from. @experimental */ + parent?: string; disableKRSEmail?: boolean; provider?: string; reqId?: IRequestTracer; diff --git a/modules/sdk-core/src/bitgo/keychain/keychains.ts b/modules/sdk-core/src/bitgo/keychain/keychains.ts index b8249b7aeb..b286bc6aac 100644 --- a/modules/sdk-core/src/bitgo/keychain/keychains.ts +++ b/modules/sdk-core/src/bitgo/keychain/keychains.ts @@ -262,6 +262,7 @@ export class Keychains implements IKeychains { 'originalPasscodeEncryptionCode', 'enterprise', 'derivedFromParentWithSeed', + 'parent', 'safeId', ] ); @@ -290,6 +291,7 @@ export class Keychains implements IKeychains { originalPasscodeEncryptionCode: params.originalPasscodeEncryptionCode, enterprise: params.enterprise, derivedFromParentWithSeed: params.derivedFromParentWithSeed, + parent: params.parent, disableKRSEmail: params.disableKRSEmail, krsSpecific: params.krsSpecific, keyShares: params.keyShares, diff --git a/modules/sdk-core/src/bitgo/safe/iSafe.ts b/modules/sdk-core/src/bitgo/safe/iSafe.ts index f80dab8cb9..86c5a4eb47 100644 --- a/modules/sdk-core/src/bitgo/safe/iSafe.ts +++ b/modules/sdk-core/src/bitgo/safe/iSafe.ts @@ -36,13 +36,15 @@ export interface FinalizeSafeOptions { */ export type WalletShareData = WalletShare; -// ---- per-safe operation options (bodies land in WCN-1203 / WCN-1204) ---- +// ---- per-safe operation options ---- export interface CreateSafeWalletOptions { coin: string; label: string; - type?: string; - multisigTypeVersion?: string; + passphrase: string; + type?: 'hot'; + /** `tss` throws until MPC mint lands. Defaults to `onchain`. */ + multisigType?: 'onchain' | 'tss'; } interface AddSafeMemberBase { diff --git a/modules/sdk-core/src/bitgo/safe/safe.ts b/modules/sdk-core/src/bitgo/safe/safe.ts index 24569da4c8..3d2a2271ce 100644 --- a/modules/sdk-core/src/bitgo/safe/safe.ts +++ b/modules/sdk-core/src/bitgo/safe/safe.ts @@ -4,11 +4,17 @@ * @experimental The safe client surface is experimental and may change (including breaking * changes) before the public release. */ -import { FreezeSafeBody, SafeData, SafeShareData, SafeShareState } from '@bitgo/public-types'; +import * as t from 'io-ts'; +import { FreezeSafeBody, SafeData, SafeShareData, SafeShareState, type RootKeyType } from '@bitgo/public-types'; +import { KeyCurve } from '@bitgo/statics'; +import { IBaseCoin } from '../baseCoin'; import { BitGoBase } from '../bitgoBase'; -import { decodeWithCodec } from '../utils/codecs'; +import { IncorrectPasswordError } from '../errors'; +import { decryptKeychainPrivateKey } from '../keychain'; +import { boundedInt, decodeWithCodec } from '../utils/codecs'; import { postWithCodec } from '../utils/postWithCodec'; import { Wallet } from '../wallet'; +import { InvalidRootKeychainSourceError } from '../wallet/safeKeychain'; import { AcceptSafeShareOptions, AddSafeMemberOptions, @@ -17,6 +23,50 @@ import { ISafe, WalletShareData, } from './iSafe'; +import { deriveAndSelfCheckSafeChildHardened } from './safeDerivation'; + +const SafeRootKeySlot = t.keyof({ + secp256k1Multisig: null, + ecdsaMpc: null, + eddsaMpc: null, + ed25519Multisig: null, +}); + +const GetDerivationIndexResponse = t.type({ + slot: SafeRootKeySlot, + index: boundedInt(0, 0x7fffffff, 'derivationIndex'), +}); + +const CreateWalletInSafeBody = t.strict({ + coin: t.string, + label: t.string, + type: t.literal('hot'), + multisigType: t.literal('onchain'), + keys: t.tuple([t.string]), +}); + +function onchainSlotForCoin(coin: IBaseCoin): Extract { + if (coin.getDefaultMultisigType() === 'tss') { + throw new Error('MPC safe wallet minting is not yet implemented; use a slot-1 onchain coin'); + } + const curve = coin.getConfig().primaryKeyCurve; + if (curve === KeyCurve.Secp256k1) { + return 'secp256k1Multisig'; + } + if (curve === KeyCurve.Ed25519) { + throw new Error('ed25519 coin safe wallet minting is not yet supported'); + } + throw new Error(`Coin '${coin.getChain()}' is not supported for safe wallet minting`); +} + +function userRootIdFromSafe(safe: SafeData, slot: RootKeyType): string | undefined { + const triplet = safe.rootKeys?.hot?.[slot]; + if (!triplet || triplet.length !== 3) { + return undefined; + } + const userRootId = triplet[0]; + return userRootId.length > 0 ? userRootId : undefined; +} /** * @experimental @@ -55,11 +105,73 @@ export class Safe implements ISafe { } /** - * Mint a child wallet in this safe (server-side public derivation — no ceremony). - * Body lands in WCN-1203. + * Mint a child wallet: peek the sequential index, hardened-derive the user child, + * register it public-only, then mint. Backup and BitGo children are soft-derived on the server. */ async createWallet(params: CreateSafeWalletOptions): Promise { - throw new Error('Safe.createWallet is not yet implemented (WCN-1203)'); + if (params.passphrase.length === 0) { + throw new Error('passphrase is required to mint a safe wallet'); + } + if (params.type !== undefined && params.type !== 'hot') { + throw new Error('Safe wallets are hot-only in v1'); + } + if (params.multisigType === 'tss') { + throw new Error('MPC safe wallet minting is not yet implemented; use multisigType "onchain"'); + } + + const coin = this.bitgo.coin(params.coin); + const slot = onchainSlotForCoin(coin); + + const indexResponse = await this.bitgo.get(this.url('/derivation-index')).query({ slot }).result(); + const peeked = decodeWithCodec(GetDerivationIndexResponse, indexResponse, 'GetDerivationIndexResponse'); + if (peeked.slot !== slot) { + throw new Error(`derivation-index returned slot '${peeked.slot}', expected '${slot}'`); + } + const { index } = peeked; + + const userRootId = userRootIdFromSafe(this._safe, slot) ?? userRootIdFromSafe(await this.fetchSafeData(), slot); + if (userRootId === undefined) { + throw new Error(`Safe ${this.id()} is missing rootKeys.hot.${slot}`); + } + + const keychains = coin.keychains(); + const rootKeychain = await keychains.get({ id: userRootId }); + if (rootKeychain.source !== 'user') { + throw new InvalidRootKeychainSourceError(rootKeychain.id, rootKeychain.source); + } + const rootPrv = await decryptKeychainPrivateKey(this.bitgo, rootKeychain, params.passphrase); + if (!rootPrv) { + throw new IncorrectPasswordError(); + } + + const derived = deriveAndSelfCheckSafeChildHardened(rootPrv, index); + + const child = await keychains.add({ + pub: derived.pub, + source: 'user', + keyType: 'independent', + parent: userRootId, + safeId: this.id(), + }); + const childId = child.id; + if (childId.length === 0) { + throw new Error('safe child key registration returned an empty id'); + } + const keys: [string] = [childId]; + + const response = await postWithCodec(this.bitgo, this.url('/wallets'), CreateWalletInSafeBody, { + coin: params.coin, + label: params.label, + type: 'hot', + multisigType: 'onchain', + keys, + }).result(); + return new Wallet(this.bitgo, coin, response); + } + + private async fetchSafeData(): Promise { + const response = await this.bitgo.get(this.url()).result(); + return decodeWithCodec(SafeData, response, 'SafeData'); } /** diff --git a/modules/sdk-core/src/bitgo/safe/safeDerivation.ts b/modules/sdk-core/src/bitgo/safe/safeDerivation.ts index df699b7756..8eb5ad0512 100644 --- a/modules/sdk-core/src/bitgo/safe/safeDerivation.ts +++ b/modules/sdk-core/src/bitgo/safe/safeDerivation.ts @@ -2,23 +2,37 @@ * @prettier * * Shared safe child derivation for mint and sign. - * Path: m/999999'/' where index is the mint allocation stored on the - * child key as derivedFromParentWithSeed. * - * Soft deriveKeyWithSeed (m/999999/a/b) must not be used for safe children — - * it cannot reproduce a hardened key. + * User child: hardened `m/'` from the sequential `safe.derivationIndex[slot]`. + * Backup / BitGo children are soft-derived server-side at `m/` (not here). + * + * Do not use `derivedFromParentWithSeed` / `deriveKeyWithSeed` (`m/999999/a/b`) — + * that is the custody hashed path and cannot reproduce a safe child. */ -import { bip32 } from '@bitgo/utxo-lib'; +import { bip32, BIP32Interface } from '@bitgo/utxo-lib'; -/** BIP32 purpose for safe wallet derivation (hardened). */ -export const SAFE_DERIVATION_PURPOSE = 999999; +const MAX_BIP32_INDEX = 0x7fffffff; -export function getSafeHardenedDerivationPath(index: string | number): string { - const idx = typeof index === 'number' ? String(index) : index; - if (!/^\d+$/.test(idx)) { +/** Sign-time scan cap (wallet cap plus abandoned mint increments). */ +export const MAX_SAFE_CHILD_INDEX_SCAN = 4096; + +export function parseSafeDerivationIndex(index: string | number): number { + let idx: number; + if (typeof index === 'number') { + idx = index; + } else if (/^\d+$/.test(index)) { + idx = Number(index); + } else { + throw new Error(`Invalid safe derivation index '${index}': expected a non-negative integer`); + } + if (!Number.isInteger(idx) || idx < 0 || idx > MAX_BIP32_INDEX) { throw new Error(`Invalid safe derivation index '${index}': expected a non-negative integer`); } - return `m/${SAFE_DERIVATION_PURPOSE}'/${idx}'`; + return idx; +} + +export function getSafeHardenedDerivationPath(index: string | number): string { + return `m/${parseSafeDerivationIndex(index)}'`; } export interface SafeHardenedChildKey { @@ -27,10 +41,7 @@ export interface SafeHardenedChildKey { derivationPath: string; } -/** Hardened BIP32 derive for secp256k1 multisig from a root xprv and mint index. */ -export function deriveSafeChildHardenedFromXprv(rootXprv: string, index: string | number): SafeHardenedChildKey { - const derivationPath = getSafeHardenedDerivationPath(index); - const child = bip32.fromBase58(rootXprv).derivePath(derivationPath); +function childFromNode(child: BIP32Interface, derivationPath: string): SafeHardenedChildKey { if (!child.privateKey) { throw new Error(`Failed to derive hardened safe child at ${derivationPath}`); } @@ -40,3 +51,36 @@ export function deriveSafeChildHardenedFromXprv(rootXprv: string, index: string derivationPath, }; } + +export function deriveSafeChildHardenedFromXprv(rootXprv: string, index: string | number): SafeHardenedChildKey { + const idx = parseSafeDerivationIndex(index); + const derivationPath = getSafeHardenedDerivationPath(idx); + return childFromNode(bip32.fromBase58(rootXprv).deriveHardened(idx), derivationPath); +} + +/** Re-derive and assert both results match before the child is registered. */ +export function deriveAndSelfCheckSafeChildHardened(rootXprv: string, index: string | number): SafeHardenedChildKey { + const first = deriveSafeChildHardenedFromXprv(rootXprv, index); + const second = deriveSafeChildHardenedFromXprv(rootXprv, index); + if (first.pub !== second.pub || first.prv !== second.prv) { + throw new Error(`Safe child self-check failed at ${first.derivationPath}: derivation was not deterministic`); + } + return first; +} + +/** Walk `m/0'` … `m/'` until the registered child pub matches. */ +export function deriveSafeChildHardenedMatchingPub( + rootXprv: string, + expectedPub: string, + maxIndex: number = MAX_SAFE_CHILD_INDEX_SCAN +): SafeHardenedChildKey { + const root = bip32.fromBase58(rootXprv); + const limit = parseSafeDerivationIndex(maxIndex); + for (let i = 0; i <= limit; i++) { + const derived = childFromNode(root.deriveHardened(i), getSafeHardenedDerivationPath(i)); + if (derived.pub === expectedPub) { + return derived; + } + } + throw new Error(`No hardened safe child at m/0'..m/${limit}' matched the registered public key`); +} diff --git a/modules/sdk-core/src/bitgo/wallet/safeKeychain.ts b/modules/sdk-core/src/bitgo/wallet/safeKeychain.ts index d6ef75e0b7..e80a7afe74 100644 --- a/modules/sdk-core/src/bitgo/wallet/safeKeychain.ts +++ b/modules/sdk-core/src/bitgo/wallet/safeKeychain.ts @@ -3,7 +3,7 @@ */ import { BitGoBase } from '../bitgoBase'; import { decryptKeychainPrivateKey, IKeychains, Keychain, KeychainWithEncryptedPrv } from '../keychain'; -import { deriveSafeChildHardenedFromXprv } from '../safe/safeDerivation'; +import { deriveSafeChildHardenedMatchingPub } from '../safe/safeDerivation'; import { IncorrectPasswordError } from '../errors'; export class InvalidRootKeychainSourceError extends Error { @@ -86,8 +86,8 @@ export interface ResolveSafeOwnerSigningPrvParams { /** * Resolve signing material for a safe owner (child key has no encryptedPrv). * - * Onchain secp256k1: decrypt root → hardened-derive at `derivedFromParentWithSeed` → - * verify derived pub against the registered child pub. + * Onchain secp256k1: decrypt root → walk sequential `m/'` children until the + * registered pub matches (the mint index is not stored on the child key). * TSS and ed25519 onchain: throw — do not return root material or BIP32-derive the wrong curve. * * Do not use for wallet sharing — that must not receive root key material. @@ -116,18 +116,15 @@ export async function resolveSafeOwnerSigningPrv(params: ResolveSafeOwnerSigning throw new IncorrectPasswordError(); } - if (childKeychain.derivedFromParentWithSeed === undefined) { - throw new Error(`Safe wallet ${walletId}: child keychain is missing derivedFromParentWithSeed (derivation index)`); - } - - const derived = deriveSafeChildHardenedFromXprv(rootPrv, childKeychain.derivedFromParentWithSeed); - if (!childKeychain.pub) { throw new Error(`Safe wallet ${walletId}: child keychain is missing pub for pre-sign verification`); } - if (derived.pub !== childKeychain.pub) { - throw new SafeDerivedPublicKeyMismatchError(walletId, childKeychain.pub, derived.pub); - } - return derived.prv; + try { + const derived = deriveSafeChildHardenedMatchingPub(rootPrv, childKeychain.pub); + return derived.prv; + } catch (e) { + const detail = e instanceof Error ? e.message : String(e); + throw new SafeDerivedPublicKeyMismatchError(walletId, childKeychain.pub, detail); + } } diff --git a/modules/sdk-core/test/unit/bitgo/safe/safe.ts b/modules/sdk-core/test/unit/bitgo/safe/safe.ts index 8ec2830349..b4122eb095 100644 --- a/modules/sdk-core/test/unit/bitgo/safe/safe.ts +++ b/modules/sdk-core/test/unit/bitgo/safe/safe.ts @@ -1,7 +1,10 @@ import * as sinon from 'sinon'; import 'should'; import { SafeData } from '@bitgo/public-types'; -import { Safe } from '../../../../src'; +import { IncorrectPasswordError, Safe, deriveSafeChildHardenedFromXprv } from '../../../../src'; + +const ROOT_XPRV = + 'xprv9s21ZrQH143K3hekyNj7TciR4XNYe1kMj68W2ipjJGNHETWP7o42AjDnSPgKhdZ4x8NBAvaL72RrXjuXNdmkMqLERZza73oYugGtbLFXG8g'; describe('Safe', function () { let safe: Safe; @@ -10,10 +13,24 @@ describe('Safe', function () { // wire-shaped SafeData (timestamps as ISO strings) for mocked REST responses that get decoded let safeDataWire: any; + const secp256k1Multisig: [string, string, string] = ['user-root-id', 'backup-root-id', 'bitgo-root-id']; + const ecdsaMpc: [string, string, string] = ['ecdsa-user', 'ecdsa-backup', 'ecdsa-bitgo']; + const eddsaMpc: [string, string, string] = ['eddsa-user', 'eddsa-backup', 'eddsa-bitgo']; + const ed25519Multisig: [string, string, string] = ['ed-user', 'ed-backup', 'ed-bitgo']; + const rootKeys = { + hot: { + secp256k1Multisig, + ecdsaMpc, + eddsaMpc, + ed25519Multisig, + }, + }; + beforeEach(function () { mockBitGo = { url: sinon.stub().callsFake((path: string) => path), post: sinon.stub(), + get: sinon.stub(), }; safeData = { id: 'test-safe-id', @@ -23,6 +40,7 @@ describe('Safe', function () { creator: 'creator-id', users: [{ userId: 'creator-id', permissions: ['admin', 'spend'] }], createdAt: new Date('2026-07-07T00:00:00.000Z'), + rootKeys, }; safeDataWire = { id: 'test-safe-id', @@ -32,6 +50,7 @@ describe('Safe', function () { creator: 'creator-id', users: [{ userId: 'creator-id', permissions: ['admin', 'spend'] }], createdAt: '2026-07-07T00:00:00.000Z', + rootKeys, }; safe = new Safe(mockBitGo, safeData); }); @@ -89,11 +108,7 @@ describe('Safe', function () { }); }); - describe('member/share methods are stubbed (WCN-1203 / WCN-1204)', function () { - it('createWallet throws not-implemented (WCN-1203)', async function () { - await safe.createWallet({ coin: 'tbtc', label: 'w' }).should.be.rejectedWith(/WCN-1203/); - }); - + describe('member/share methods are stubbed (WCN-1204)', function () { it('addMember throws not-implemented (WCN-1204)', async function () { await safe.addMember({ userId: 'u', permissions: ['view'] }).should.be.rejectedWith(/WCN-1204/); }); @@ -110,4 +125,132 @@ describe('Safe', function () { await safe.acceptShare({ safeShareId: 's' }).should.be.rejectedWith(/WCN-1204/); }); }); + + describe('createWallet (WCN-1203)', function () { + const childAt0 = deriveSafeChildHardenedFromXprv(ROOT_XPRV, 0); + let keychainsAdd: sinon.SinonStub; + let keychainsGet: sinon.SinonStub; + let derivationQuery: sinon.SinonStub; + let mintSend: sinon.SinonStub; + + function stubCoin(primaryKeyCurve: string, opts: { getDefaultMultisigType?: string } = {}) { + keychainsGet = sinon.stub().resolves({ + id: 'user-root-id', + source: 'user', + encryptedPrv: `enc:${ROOT_XPRV}`, + pub: 'root-xpub', + type: 'independent', + }); + 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 }), + getDefaultMultisigType: sinon.stub().returns(opts.getDefaultMultisigType), + supportsTss: sinon.stub().returns(false), + keychains: sinon.stub().returns({ get: keychainsGet, add: keychainsAdd }), + }); + } + + beforeEach(function () { + stubCoin('secp256k1'); + mockBitGo.decrypt = sinon.stub().callsFake(({ input, password }: { input: string; password: string }) => { + if (password !== 'pw') { + throw new Error('bad password'); + } + if (input.startsWith('enc:')) { + return Promise.resolve(input.slice(4)); + } + throw new Error('bad ciphertext'); + }); + derivationQuery = sinon.stub().returns({ + result: sinon.stub().resolves({ slot: 'secp256k1Multisig', index: 0 }), + }); + mockBitGo.get.returns({ query: derivationQuery }); + const mintResult = sinon.stub().resolves({ + id: 'wallet-id', + coin: 'tbtc', + keys: ['child-key-id', 'backup-child', 'bitgo-child'], + type: 'hot', + multisigType: 'onchain', + enterprise: 'test-enterprise-id', + safeId: 'test-safe-id', + }); + mintSend = sinon.stub().returns({ result: mintResult }); + mockBitGo.post.returns({ send: mintSend }); + }); + + it('peeks the index, registers a public-only user child, and mints', async function () { + const wallet = await safe.createWallet({ coin: 'tbtc', label: 'desk 1', passphrase: 'pw' }); + + derivationQuery.calledOnceWithExactly({ slot: 'secp256k1Multisig' }).should.be.true(); + keychainsGet.calledOnceWithExactly({ id: 'user-root-id' }).should.be.true(); + keychainsAdd.calledOnce.should.be.true(); + const addArgs = keychainsAdd.firstCall.args[0]; + addArgs.should.eql({ + pub: childAt0.pub, + source: 'user', + keyType: 'independent', + parent: 'user-root-id', + safeId: 'test-safe-id', + }); + addArgs.should.not.have.property('encryptedPrv'); + addArgs.should.not.have.property('derivedFromParentWithSeed'); + + mockBitGo.post.calledWith('/enterprise/test-enterprise-id/safes/test-safe-id/wallets').should.be.true(); + mintSend.firstCall.args[0].should.eql({ + coin: 'tbtc', + label: 'desk 1', + type: 'hot', + multisigType: 'onchain', + keys: ['child-key-id'], + }); + wallet.id().should.equal('wallet-id'); + const mintedSafeId = wallet.safeId(); + if (mintedSafeId === undefined) { + throw new Error('expected minted wallet to include safeId'); + } + mintedSafeId.should.equal('test-safe-id'); + }); + + it('rejects TSS minting', async function () { + await safe + .createWallet({ coin: 'hteth', label: 'evm', passphrase: 'pw', multisigType: 'tss' }) + .should.be.rejectedWith(/MPC safe wallet minting is not yet implemented/); + }); + + it('rejects a TSS-default coin even without multisigType tss', async function () { + stubCoin('secp256k1', { getDefaultMultisigType: 'tss' }); + await safe + .createWallet({ coin: 'hteth', label: 'evm', passphrase: 'pw' }) + .should.be.rejectedWith(/MPC safe wallet minting is not yet implemented/); + }); + + it('rejects a peeked derivation index for the wrong slot', async function () { + derivationQuery.returns({ + result: sinon.stub().resolves({ slot: 'ecdsaMpc', index: 0 }), + }); + await safe + .createWallet({ coin: 'tbtc', label: 'desk 1', passphrase: 'pw' }) + .should.be.rejectedWith(/returned slot 'ecdsaMpc'/); + }); + + it('rejects ed25519 onchain coins', async function () { + stubCoin('ed25519'); + await safe + .createWallet({ coin: 'txlm', label: 'xlm', passphrase: 'pw' }) + .should.be.rejectedWith(/ed25519 coin safe wallet minting is not yet supported/); + }); + + it('rejects an empty passphrase', async function () { + await safe + .createWallet({ coin: 'tbtc', label: 'w', passphrase: '' }) + .should.be.rejectedWith(/passphrase is required/); + }); + + it('rejects a wrong passphrase', async function () { + await safe + .createWallet({ coin: 'tbtc', label: 'w', passphrase: 'nope' }) + .should.be.rejectedWith(IncorrectPasswordError); + }); + }); }); diff --git a/modules/sdk-core/test/unit/bitgo/wallet/safeGetUserPrv.ts b/modules/sdk-core/test/unit/bitgo/wallet/safeGetUserPrv.ts index 11609867cd..57a2ef83bf 100644 --- a/modules/sdk-core/test/unit/bitgo/wallet/safeGetUserPrv.ts +++ b/modules/sdk-core/test/unit/bitgo/wallet/safeGetUserPrv.ts @@ -101,24 +101,20 @@ describe('WCN-1200 safe child getUserPrv root-fetch detour', function () { } describe('safeDerivation', function () { - it('builds the hardened path from the mint index', function () { - getSafeHardenedDerivationPath(123).should.eql("m/999999'/123'"); - getSafeHardenedDerivationPath('0').should.eql("m/999999'/0'"); + it('builds the hardened path from the sequential mint index', function () { + getSafeHardenedDerivationPath(123).should.eql("m/123'"); + getSafeHardenedDerivationPath('0').should.eql("m/0'"); }); it('rejects a non-integer index', function () { (() => getSafeHardenedDerivationPath('abc')).should.throw(/Invalid safe derivation index/); + (() => getSafeHardenedDerivationPath('')).should.throw(/Invalid safe derivation index/); + (() => getSafeHardenedDerivationPath('1e2')).should.throw(/Invalid safe derivation index/); }); it('hardened-derives a child that differs from soft deriveKeyWithSeed', function () { - hardened.derivationPath.should.eql("m/999999'/123'"); + hardened.derivationPath.should.eql("m/123'"); hardened.prv.should.not.eql(softDerivedPrv); - hardened.prv.should.eql( - 'xprv9wMxE3idjgW7UoSodEZgYpy7aSzt32GC7j63s277VwkRbVvnkRubmFqZ4UUghHVTaSbdHZA3NM8FuwH4CoTQzaVzzUh1BwKcNYn17NczoQy' - ); - hardened.pub.should.eql( - 'xpub6AMJdZFXa44QhHXGjG6guxur8UqNSUz3Ux1efQWj4HHQUJFwHyDrK4A2ukru4QZ9PfhTYbPLBNYFL7gbdhTidSppW1aQ9QgYPT5cBFmoDEu' - ); }); }); @@ -247,7 +243,6 @@ describe('WCN-1200 safe child getUserPrv root-fetch detour', function () { pub: hardened.pub, type: 'independent', parent: rootKeyId, - derivedFromParentWithSeed: '123', }, walletPassphrase: passphrase, }); @@ -258,29 +253,6 @@ describe('WCN-1200 safe child getUserPrv root-fetch detour', function () { mockBaseCoin.deriveKeyWithSeed.notCalled.should.be.true(); }); - it('throws when onchain safe owner is missing derivedFromParentWithSeed', async function () { - const wallet = makeWallet({ safeId: 'safe-id-1' }); - keychainsGetStub.resolves({ - id: rootKeyId, - source: 'user', - encryptedPrv: `enc:${prv}`, - type: 'independent', - pub: 'root-pub', - }); - - await wallet - .getUserPrv({ - keychain: { - id: 'child-key', - pub: hardened.pub, - type: 'independent', - parent: rootKeyId, - }, - walletPassphrase: passphrase, - }) - .should.be.rejectedWith(/missing derivedFromParentWithSeed/); - }); - it('fails closed for TSS safe owner instead of returning the root prv', async function () { const wallet = makeWallet({ safeId: 'safe-id-1', multisigType: 'tss' });