44 * @experimental The safe client surface is experimental and may change (including breaking
55 * changes) before the public release.
66 */
7- import { FreezeSafeBody , SafeData , SafeShareData , SafeShareState } from '@bitgo/public-types' ;
7+ import * as t from 'io-ts' ;
8+ import { FreezeSafeBody , SafeData , SafeShareData , SafeShareState , type RootKeyType } from '@bitgo/public-types' ;
9+ import { KeyCurve } from '@bitgo/statics' ;
10+ import { IBaseCoin } from '../baseCoin' ;
811import { BitGoBase } from '../bitgoBase' ;
9- import { decodeWithCodec } from '../utils/codecs' ;
12+ import { IncorrectPasswordError } from '../errors' ;
13+ import { decryptKeychainPrivateKey } from '../keychain' ;
14+ import { boundedInt , decodeWithCodec } from '../utils/codecs' ;
1015import { postWithCodec } from '../utils/postWithCodec' ;
1116import { Wallet } from '../wallet' ;
17+ import { InvalidRootKeychainSourceError } from '../wallet/safeKeychain' ;
1218import {
1319 AcceptSafeShareOptions ,
1420 AddSafeMemberOptions ,
@@ -17,6 +23,50 @@ import {
1723 ISafe ,
1824 WalletShareData ,
1925} from './iSafe' ;
26+ import { deriveAndSelfCheckSafeChildHardened } from './safeDerivation' ;
27+
28+ const SafeRootKeySlot = t . keyof ( {
29+ secp256k1Multisig : null ,
30+ ecdsaMpc : null ,
31+ eddsaMpc : null ,
32+ ed25519Multisig : null ,
33+ } ) ;
34+
35+ const GetDerivationIndexResponse = t . type ( {
36+ slot : SafeRootKeySlot ,
37+ index : boundedInt ( 0 , 0x7fffffff , 'derivationIndex' ) ,
38+ } ) ;
39+
40+ const CreateWalletInSafeBody = t . strict ( {
41+ coin : t . string ,
42+ label : t . string ,
43+ type : t . literal ( 'hot' ) ,
44+ multisigType : t . literal ( 'onchain' ) ,
45+ keys : t . tuple ( [ t . string ] ) ,
46+ } ) ;
47+
48+ function onchainSlotForCoin ( coin : IBaseCoin ) : Extract < RootKeyType , 'secp256k1Multisig' > {
49+ if ( coin . getDefaultMultisigType ( ) === 'tss' ) {
50+ throw new Error ( 'MPC safe wallet minting is not yet implemented; use a slot-1 onchain coin' ) ;
51+ }
52+ const curve = coin . getConfig ( ) . primaryKeyCurve ;
53+ if ( curve === KeyCurve . Secp256k1 ) {
54+ return 'secp256k1Multisig' ;
55+ }
56+ if ( curve === KeyCurve . Ed25519 ) {
57+ throw new Error ( 'ed25519 coin safe wallet minting is not yet supported' ) ;
58+ }
59+ throw new Error ( `Coin '${ coin . getChain ( ) } ' is not supported for safe wallet minting` ) ;
60+ }
61+
62+ function userRootIdFromSafe ( safe : SafeData , slot : RootKeyType ) : string | undefined {
63+ const triplet = safe . rootKeys ?. hot ?. [ slot ] ;
64+ if ( ! triplet || triplet . length !== 3 ) {
65+ return undefined ;
66+ }
67+ const userRootId = triplet [ 0 ] ;
68+ return userRootId . length > 0 ? userRootId : undefined ;
69+ }
2070
2171/**
2272 * @experimental
@@ -55,11 +105,73 @@ export class Safe implements ISafe {
55105 }
56106
57107 /**
58- * Mint a child wallet in this safe (server-side public derivation — no ceremony).
59- * Body lands in WCN-1203 .
108+ * Mint a child wallet: peek the sequential index, hardened-derive the user child,
109+ * register it public-only, then mint. Backup and BitGo children are soft-derived on the server .
60110 */
61111 async createWallet ( params : CreateSafeWalletOptions ) : Promise < Wallet > {
62- throw new Error ( 'Safe.createWallet is not yet implemented (WCN-1203)' ) ;
112+ if ( params . passphrase . length === 0 ) {
113+ throw new Error ( 'passphrase is required to mint a safe wallet' ) ;
114+ }
115+ if ( params . type !== undefined && params . type !== 'hot' ) {
116+ throw new Error ( 'Safe wallets are hot-only in v1' ) ;
117+ }
118+ if ( params . multisigType === 'tss' ) {
119+ throw new Error ( 'MPC safe wallet minting is not yet implemented; use multisigType "onchain"' ) ;
120+ }
121+
122+ const coin = this . bitgo . coin ( params . coin ) ;
123+ const slot = onchainSlotForCoin ( coin ) ;
124+
125+ const indexResponse = await this . bitgo . get ( this . url ( '/derivation-index' ) ) . query ( { slot } ) . result ( ) ;
126+ const peeked = decodeWithCodec ( GetDerivationIndexResponse , indexResponse , 'GetDerivationIndexResponse' ) ;
127+ if ( peeked . slot !== slot ) {
128+ throw new Error ( `derivation-index returned slot '${ peeked . slot } ', expected '${ slot } '` ) ;
129+ }
130+ const { index } = peeked ;
131+
132+ const userRootId = userRootIdFromSafe ( this . _safe , slot ) ?? userRootIdFromSafe ( await this . fetchSafeData ( ) , slot ) ;
133+ if ( userRootId === undefined ) {
134+ throw new Error ( `Safe ${ this . id ( ) } is missing rootKeys.hot.${ slot } ` ) ;
135+ }
136+
137+ const keychains = coin . keychains ( ) ;
138+ const rootKeychain = await keychains . get ( { id : userRootId } ) ;
139+ if ( rootKeychain . source !== 'user' ) {
140+ throw new InvalidRootKeychainSourceError ( rootKeychain . id , rootKeychain . source ) ;
141+ }
142+ const rootPrv = await decryptKeychainPrivateKey ( this . bitgo , rootKeychain , params . passphrase ) ;
143+ if ( ! rootPrv ) {
144+ throw new IncorrectPasswordError ( ) ;
145+ }
146+
147+ const derived = deriveAndSelfCheckSafeChildHardened ( rootPrv , index ) ;
148+
149+ const child = await keychains . add ( {
150+ pub : derived . pub ,
151+ source : 'user' ,
152+ keyType : 'independent' ,
153+ parent : userRootId ,
154+ safeId : this . id ( ) ,
155+ } ) ;
156+ const childId = child . id ;
157+ if ( childId . length === 0 ) {
158+ throw new Error ( 'safe child key registration returned an empty id' ) ;
159+ }
160+ const keys : [ string ] = [ childId ] ;
161+
162+ const response = await postWithCodec ( this . bitgo , this . url ( '/wallets' ) , CreateWalletInSafeBody , {
163+ coin : params . coin ,
164+ label : params . label ,
165+ type : 'hot' ,
166+ multisigType : 'onchain' ,
167+ keys,
168+ } ) . result ( ) ;
169+ return new Wallet ( this . bitgo , coin , response ) ;
170+ }
171+
172+ private async fetchSafeData ( ) : Promise < SafeData > {
173+ const response = await this . bitgo . get ( this . url ( ) ) . result ( ) ;
174+ return decodeWithCodec ( SafeData , response , 'SafeData' ) ;
63175 }
64176
65177 /**
0 commit comments