Skip to content

Commit 009db3a

Browse files
feat: add sdk-core vault module interfaces and scaffolding
Net-new sdk-core/src/bitgo/vault module mirroring the EnterpriseData/ Enterprise and WalletData/Wallet conventions: iVault/iVaults interfaces (per API Changes v1 §3), the Vault and Vaults classes, and the barrel. Vault implements getters, url(), toJSON(), and freeze/archive REST plumbing; createWallet, member, and share methods are stubbed for WCN-1203/WCN-1204. Vaults lifecycle methods (createVault, initialize, finalize, list, get) are stubbed pending Phase 2/3 (WCN-1175/1177). Wires an Enterprise.vaults() accessor and the bitgo barrel export. WCN-1192 TICKET: WCN-1192
1 parent f328521 commit 009db3a

10 files changed

Lines changed: 595 additions & 0 deletions

File tree

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { BitGoBase } from '../bitgoBase';
77
import { EnterpriseData, EnterpriseFeatureFlag, IEnterprise } from '../enterprise';
88
import { getFirstPendingTransaction } from '../internal';
99
import { ListWalletOptions, Wallet } from '../wallet';
10+
import { Vaults } from '../vault';
1011
import { BitGoProofSignatures, EcdsaUtils, SerializedNtildeWithVerifiers } from '../utils/tss/ecdsa';
1112
import { EcdsaTypes } from '@bitgo/sdk-lib-mpc';
1213
import { verifyEcdhSignature } from '../ecdh';
@@ -249,4 +250,11 @@ export class Enterprise implements IEnterprise {
249250
hasFeatureFlags(flags: EnterpriseFeatureFlag[]): boolean {
250251
return flags.every((targetFlag) => this._enterprise.featureFlags?.includes(targetFlag));
251252
}
253+
254+
/**
255+
* Get the vaults collection accessor scoped to this Enterprise
256+
*/
257+
vaults(): Vaults {
258+
return new Vaults(this.bitgo, this.baseCoin, this.id);
259+
}
252260
}

modules/sdk-core/src/bitgo/enterprise/iEnterprise.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { IWallet } from '../wallet';
33
import { Buffer } from 'buffer';
44
import { BitGoProofSignatures, SerializedNtildeWithVerifiers } from '../utils/tss/ecdsa';
55
import { EcdhDerivedKeypair } from '../keychain';
6+
import { IVaults } from '../vault';
67

78
// useEnterpriseEcdsaTssChallenge is deprecated
89
export type EnterpriseFeatureFlag = 'useEnterpriseEcdsaTssChallenge';
@@ -39,4 +40,5 @@ export interface IEnterprise {
3940
bitgoNitroChallenge: SerializedNtildeWithVerifiers
4041
): Promise<void>;
4142
hasFeatureFlags(flags: EnterpriseFeatureFlag[]): boolean;
43+
vaults(): IVaults;
4244
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export * from './tss';
2727
export { sendSignatureShare } from './tss';
2828
export * from './types';
2929
export * from './utils';
30+
export * from './vault';
3031
export * from './wallet';
3132
export * from './webhook';
3233
export { bitcoinUtil };
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/**
2+
* @prettier
3+
*/
4+
import type { FreezeOptions, Wallet, WalletShare } from '../wallet';
5+
6+
/**
7+
* The four static root-key slots of a vault, keyed by (curve, scheme):
8+
* - secp256k1Multisig ① — UTXO/XRP/XTZ/TRX/EOS
9+
* - ecdsaMpc ② — EVM/Cosmos (DKLS)
10+
* - eddsaMpc ③ — SOL/SUI/NEAR/TON/APT/DOT
11+
* - ed25519Multisig ④ — ALGO/XLM/HBAR
12+
*/
13+
export type RootKeyType = 'secp256k1Multisig' | 'ecdsaMpc' | 'eddsaMpc' | 'ed25519Multisig';
14+
15+
export type VaultPermission = 'view' | 'spend' | 'admin' | 'dapp';
16+
17+
/**
18+
* The 12 static root key ids, by (curve, scheme). Each entry is an ordered
19+
* [userKeyId, backupKeyId, bitgoKeyId] triplet — same shape and order as wallet.keys[].
20+
*/
21+
export type VaultRootKeys = Record<RootKeyType, [userKeyId: string, backupKeyId: string, bitgoKeyId: string]>;
22+
23+
export interface VaultMembershipData {
24+
userId: string; // new-model naming convention
25+
permissions: VaultPermission[];
26+
needsRecovery?: boolean;
27+
}
28+
29+
/**
30+
* A pending UMS spend grant awaiting a key share — mirror of the wallet's walletShareRequests[].
31+
* Returned to spenders/admins and serviced via addMember.
32+
*/
33+
export interface VaultShareRequest {
34+
userId: string;
35+
permissions: VaultPermission[];
36+
createdAt: string;
37+
}
38+
39+
export interface VaultData {
40+
id: string;
41+
enterpriseId: string;
42+
label: string;
43+
// freeze is NOT a status — a frozen vault stays 'active' with the freeze field set (wallet precedent)
44+
status: 'initializing' | 'active' | 'archived';
45+
creator: string;
46+
users: VaultMembershipData[];
47+
vaultShareRequests?: VaultShareRequest[];
48+
freeze?: { time?: string; expires?: string; reason?: string };
49+
rootKeys?: VaultRootKeys; // ordered, like wallet.keys
50+
archivedAt?: string;
51+
createdAt: string;
52+
}
53+
54+
export interface InitializeVaultOptions {
55+
label: string; // Phase 1 carries no key material — key generation happens in Phase 2 via the existing keychain APIs
56+
}
57+
58+
// Phase 3 — the client hands back the 12 key ids it created in Phase 2:
59+
export interface FinalizeVaultOptions {
60+
rootKeys: VaultRootKeys;
61+
}
62+
63+
/** Vault key-share states — identical to WalletShare states, no new states. */
64+
export type VaultShareState = 'pendingapproval' | 'active' | 'accepted' | 'canceled' | 'rejected';
65+
66+
/**
67+
* One of the 4 root USER keyshares carried on a VaultShare, ECDH-re-encrypted to the recipient.
68+
* Body semantics land in the Part VI SDK ticket (WCN-1204).
69+
*/
70+
export interface VaultShareKeychain {
71+
rootKeyType: RootKeyType;
72+
rootKeyId: string;
73+
encryptedPrv: string;
74+
publicIdentifier: string;
75+
fromPubKey: string;
76+
toPubKey: string;
77+
path: string;
78+
}
79+
80+
export interface VaultShareData {
81+
id: string;
82+
enterpriseId: string;
83+
vaultId: string;
84+
vaultLabel?: string;
85+
fromUser: string;
86+
toUser: string;
87+
permissions: VaultPermission[];
88+
state: VaultShareState;
89+
message?: string;
90+
pendingApprovalId?: string;
91+
isUMSInitiated?: boolean;
92+
keychains?: VaultShareKeychain[];
93+
createdAt: string;
94+
updatedAt?: string;
95+
}
96+
97+
/**
98+
* Sharing ONE vault wallet with a non-member rides the existing wallet-share handshake (FR-13),
99+
* so the result is the existing WalletShare shape.
100+
*/
101+
export type WalletShareData = WalletShare;
102+
103+
// ---- per-vault operation options (bodies land in WCN-1203 / WCN-1204) ----
104+
105+
export interface CreateVaultWalletOptions {
106+
coin: string;
107+
label: string;
108+
type?: string;
109+
multisigType?: string;
110+
multisigTypeVersion?: string;
111+
}
112+
113+
export interface AddVaultMemberOptions {
114+
userId?: string;
115+
email?: string;
116+
permissions: VaultPermission[];
117+
// required when 'spend' is included — the 4 root user keys ECDH-re-encrypted to the invitee
118+
keychains?: VaultShareKeychain[];
119+
message?: string;
120+
disableEmail?: boolean;
121+
}
122+
123+
export interface AddVaultWalletMemberOptions {
124+
walletId: string;
125+
email?: string;
126+
permissions?: string[];
127+
message?: string;
128+
}
129+
130+
export interface AcceptVaultShareOptions {
131+
vaultShareId: string;
132+
userPassword?: string;
133+
newWalletPassphrase?: string;
134+
overrideEncryptedPrv?: string;
135+
}
136+
137+
export interface IVault {
138+
id(): string;
139+
enterpriseId(): string;
140+
label(): string;
141+
status(): VaultData['status'];
142+
url(extra?: string): string;
143+
createWallet(params: CreateVaultWalletOptions): Promise<Wallet>;
144+
// whole-vault: view/admin/spend; spend opens a key share (also how a spender services a
145+
// vaultShareRequests entry in UMS orgs)
146+
addMember(params: AddVaultMemberOptions): Promise<VaultData>;
147+
// share ONE vault wallet (FR-13), not the whole vault
148+
addMemberToWallet(params: AddVaultWalletMemberOptions): Promise<WalletShareData>;
149+
listShares(params?: { state?: VaultShareState }): Promise<VaultShareData[]>;
150+
acceptShare(params: AcceptVaultShareOptions): Promise<VaultShareData>;
151+
freeze(params?: FreezeOptions): Promise<VaultData>;
152+
archive(): Promise<VaultData>;
153+
toJSON(): VaultData;
154+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/**
2+
* @prettier
3+
*/
4+
import { InitializeVaultOptions, FinalizeVaultOptions } from './iVault';
5+
import { Vault } from './vault';
6+
7+
/**
8+
* Options for the createVault orchestrator.
9+
*
10+
* v1 targets the HOT custody model only: the multisig root user/backup keys are generated locally
11+
* by the SDK, encrypted with `passphrase`, and registered on BitGo; the MPC roots run the standard
12+
* hot ceremonies with the same passphrase. Self-managed cold keys and custodial vaults are out of
13+
* scope for v1.
14+
*/
15+
export interface CreateVaultOptions {
16+
label: string;
17+
passphrase: string; // encrypts the locally-generated multisig user/backup prvs; shared with the MPC ceremonies
18+
}
19+
20+
export interface ListVaultsOptions {
21+
cursor?: string; // opaque cursor from a previous response's nextCursor
22+
limit?: number;
23+
}
24+
25+
export interface GetVaultOptions {
26+
id: string;
27+
}
28+
29+
export interface IVaults {
30+
// orchestrates: initialize → 4 root key flows (vaultId-tagged) → finalize → keycard
31+
createVault(params: CreateVaultOptions): Promise<Vault>;
32+
initializeVault(params: InitializeVaultOptions): Promise<Vault>;
33+
finalizeVault(vaultId: string, params: FinalizeVaultOptions): Promise<Vault>;
34+
list(params?: ListVaultsOptions): Promise<{ vaults: Vault[]; nextCursor?: string }>;
35+
get(params: GetVaultOptions): Promise<Vault>;
36+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './iVault';
2+
export * from './iVaults';
3+
export * from './vault';
4+
export * from './vaults';
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
/**
2+
* @prettier
3+
*/
4+
import * as _ from 'lodash';
5+
import { IBaseCoin } from '../baseCoin';
6+
import { BitGoBase } from '../bitgoBase';
7+
import { FreezeOptions, Wallet } from '../wallet';
8+
import {
9+
AcceptVaultShareOptions,
10+
AddVaultMemberOptions,
11+
AddVaultWalletMemberOptions,
12+
CreateVaultWalletOptions,
13+
IVault,
14+
VaultData,
15+
VaultShareData,
16+
VaultShareState,
17+
WalletShareData,
18+
} from './iVault';
19+
20+
export class Vault implements IVault {
21+
private readonly bitgo: BitGoBase;
22+
private readonly baseCoin: IBaseCoin;
23+
public readonly _vault: VaultData;
24+
25+
constructor(bitgo: BitGoBase, baseCoin: IBaseCoin, vaultData: VaultData) {
26+
this.bitgo = bitgo;
27+
this.baseCoin = baseCoin;
28+
if (!_.isObject(vaultData)) {
29+
throw new Error('vaultData has to be an object');
30+
}
31+
if (!_.isString(vaultData.id)) {
32+
throw new Error('vault id has to be a string');
33+
}
34+
if (!_.isString(vaultData.enterpriseId)) {
35+
throw new Error('vault enterpriseId has to be a string');
36+
}
37+
this._vault = vaultData;
38+
}
39+
40+
id(): string {
41+
return this._vault.id;
42+
}
43+
44+
enterpriseId(): string {
45+
return this._vault.enterpriseId;
46+
}
47+
48+
label(): string {
49+
return this._vault.label;
50+
}
51+
52+
status(): VaultData['status'] {
53+
return this._vault.status;
54+
}
55+
56+
/**
57+
* Enterprise-scoped v2 URL for this vault, e.g. /api/v2/enterprise/:eId/vaults/:vId
58+
* @param extra
59+
*/
60+
url(extra = ''): string {
61+
return this.bitgo.url(`/enterprise/${this.enterpriseId()}/vaults/${this.id()}${extra}`, 2);
62+
}
63+
64+
/**
65+
* Mint a child wallet in this vault (server-side public derivation — no ceremony).
66+
* Body lands in WCN-1203.
67+
*/
68+
async createWallet(params: CreateVaultWalletOptions): Promise<Wallet> {
69+
throw new Error('Vault.createWallet is not yet implemented (WCN-1203)');
70+
}
71+
72+
/**
73+
* Add a member to the whole vault (view/admin/spend). Spend opens a key share.
74+
* Body lands in WCN-1204.
75+
*/
76+
async addMember(params: AddVaultMemberOptions): Promise<VaultData> {
77+
throw new Error('Vault.addMember is not yet implemented (WCN-1204)');
78+
}
79+
80+
/**
81+
* Share ONE vault wallet with a non-member via the existing wallet-share handshake (FR-13).
82+
* Body lands in WCN-1204.
83+
*/
84+
async addMemberToWallet(params: AddVaultWalletMemberOptions): Promise<WalletShareData> {
85+
throw new Error('Vault.addMemberToWallet is not yet implemented (WCN-1204)');
86+
}
87+
88+
/**
89+
* List the vault key shares visible to the caller.
90+
* Body lands in WCN-1204.
91+
*/
92+
async listShares(params: { state?: VaultShareState } = {}): Promise<VaultShareData[]> {
93+
throw new Error('Vault.listShares is not yet implemented (WCN-1204)');
94+
}
95+
96+
/**
97+
* Accept a vault key share addressed to the caller.
98+
* Body lands in WCN-1204.
99+
*/
100+
async acceptShare(params: AcceptVaultShareOptions): Promise<VaultShareData> {
101+
throw new Error('Vault.acceptShare is not yet implemented (WCN-1204)');
102+
}
103+
104+
/**
105+
* Freeze the vault — blocks withdrawals on all vault wallets. Vault stays 'active'.
106+
* @param params
107+
*/
108+
async freeze(params: FreezeOptions = {}): Promise<VaultData> {
109+
return (await this.bitgo.post(this.url('/freeze')).send(params).result()) as VaultData;
110+
}
111+
112+
/**
113+
* Archive the vault. Requires every vault wallet to already be archived; also the abandonment
114+
* path for a stuck 'initializing' vault.
115+
*/
116+
async archive(): Promise<VaultData> {
117+
return (await this.bitgo.post(this.url('/archive')).send().result()) as VaultData;
118+
}
119+
120+
toJSON(): VaultData {
121+
return this._vault;
122+
}
123+
}

0 commit comments

Comments
 (0)