Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions modules/sdk-core/src/bitgo/enterprise/enterprise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { BitGoBase } from '../bitgoBase';
import { EnterpriseData, EnterpriseFeatureFlag, IEnterprise } from '../enterprise';
import { getFirstPendingTransaction } from '../internal';
import { ListWalletOptions, Wallet } from '../wallet';
import { Vaults } from '../vault';
import { BitGoProofSignatures, EcdsaUtils, SerializedNtildeWithVerifiers } from '../utils/tss/ecdsa';
import { EcdsaTypes } from '@bitgo/sdk-lib-mpc';
import { verifyEcdhSignature } from '../ecdh';
Expand Down Expand Up @@ -249,4 +250,12 @@ export class Enterprise implements IEnterprise {
hasFeatureFlags(flags: EnterpriseFeatureFlag[]): boolean {
return flags.every((targetFlag) => this._enterprise.featureFlags?.includes(targetFlag));
}

/**
* Get the vaults collection accessor scoped to this Enterprise
* @experimental
*/
vaults(): Vaults {
return new Vaults(this.bitgo, this.id);
}
}
3 changes: 3 additions & 0 deletions modules/sdk-core/src/bitgo/enterprise/iEnterprise.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { IWallet } from '../wallet';
import { Buffer } from 'buffer';
import { BitGoProofSignatures, SerializedNtildeWithVerifiers } from '../utils/tss/ecdsa';
import { EcdhDerivedKeypair } from '../keychain';
import { IVaults } from '../vault';

// useEnterpriseEcdsaTssChallenge is deprecated
export type EnterpriseFeatureFlag = 'useEnterpriseEcdsaTssChallenge';
Expand Down Expand Up @@ -39,4 +40,6 @@ export interface IEnterprise {
bitgoNitroChallenge: SerializedNtildeWithVerifiers
): Promise<void>;
hasFeatureFlags(flags: EnterpriseFeatureFlag[]): boolean;
/** @experimental */
vaults(): IVaults;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets add an @experimental jsdoc, in case we need to make breaking changes before the public release.

}
1 change: 1 addition & 0 deletions modules/sdk-core/src/bitgo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export * from './tss';
export { sendSignatureShare } from './tss';
export * from './types';
export * from './utils';
export * from './vault';
export * from './wallet';
export * from './webhook';
export { bitcoinUtil };
Expand Down
178 changes: 178 additions & 0 deletions modules/sdk-core/src/bitgo/vault/codecs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/**
* @prettier
*
* io-ts codecs for the vault REST surface. These are the single source of truth for the
* vault data shapes: the TypeScript interfaces in `iVault.ts` are derived from them via
* `t.TypeOf`, request bodies are encoded with `postWithCodec`, and responses are decoded
* (and validated) with `decodeWithCodec` — so there are no `as VaultData` casts.
*
* Timestamps use `DateFromISOString`: the wire representation stays an ISO-8601 string while
* the decoded object exposes a real `Date`.
*/
import * as t from 'io-ts';
import { DateFromISOString } from 'io-ts-types';

/**
* The four static root-key slots of a vault, keyed by (curve, scheme):
* - secp256k1Multisig ① — ex. UTXO/XRP/XTZ/TRX/EOS
* - ecdsaMpc ② — ex. EVM/Cosmos (DKLS)
* - eddsaMpc ③ — ex. SOL/SUI/NEAR/TON/APT/DOT
* - ed25519Multisig ④ — ex. ALGO/XLM/HBAR
*/
export const RootKeyType = t.keyof(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once we finalize, we should consider moving these codecs to public-types. Having it in the sdk for now is fine though.

{
secp256k1Multisig: null,
ecdsaMpc: null,
eddsaMpc: null,
ed25519Multisig: null,
},
'RootKeyType'
);

export const VaultPermission = t.keyof(
{
view: null,
spend: null,
admin: null,
dapp: null,
},
'VaultPermission'
);

/** An ordered [userKeyId, backupKeyId, bitgoKeyId] triplet — same shape/order as wallet.keys[]. */
export const RootKeyTriplet = t.tuple([t.string, t.string, t.string], 'RootKeyTriplet');

/** The 12 static root key ids, by (curve, scheme). */
export const VaultRootKeys = t.type(
{
secp256k1Multisig: RootKeyTriplet,
ecdsaMpc: RootKeyTriplet,
eddsaMpc: RootKeyTriplet,
ed25519Multisig: RootKeyTriplet,
},
'VaultRootKeys'
);

export const VaultMembershipData = t.intersection(
[
t.type({
userId: t.string,
permissions: t.array(VaultPermission),
}),
t.partial({
needsRecovery: t.boolean,
}),
],
'VaultMembershipData'
);

/** A pending UMS spend grant awaiting a key share — mirror of the wallet's walletShareRequests[]. */
export const VaultShareRequest = t.type(
{
userId: t.string,
permissions: t.array(VaultPermission),
createdAt: DateFromISOString,
},
'VaultShareRequest'
);

export const VaultFreeze = t.partial(
{
time: DateFromISOString,
expires: DateFromISOString,
reason: t.string,
},
'VaultFreeze'
);

export const VaultStatus = t.keyof(
{
initializing: null,
active: null,
archived: null,
},
'VaultStatus'
);

export const VaultData = t.intersection(
[
t.type({
id: t.string,
enterpriseId: t.string,
label: t.string,
// freeze is NOT a status — a frozen vault stays 'active' with the freeze field set (wallet precedent)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// freeze is NOT a status — a frozen vault stays 'active' with the freeze field set (wallet precedent)

status: VaultStatus,
creator: t.string,
users: t.array(VaultMembershipData),
createdAt: DateFromISOString,
}),
t.partial({
vaultShareRequests: t.array(VaultShareRequest),
freeze: VaultFreeze,
rootKeys: VaultRootKeys,
archivedAt: DateFromISOString,
}),
],
'VaultData'
);

/** Vault key-share states — identical to WalletShare states, no new states. */
export const VaultShareState = t.keyof(
{
pendingapproval: null,
active: null,
accepted: null,
canceled: null,
rejected: null,
},
'VaultShareState'
);

/** One of the 4 root USER keyshares carried on a VaultShare, ECDH-re-encrypted to the recipient. */
export const VaultShareKeychain = t.type(
{
rootKeyType: RootKeyType,
rootKeyId: t.string,
encryptedPrv: t.string,
publicIdentifier: t.string,
fromPubKey: t.string,
toPubKey: t.string,
path: t.string,
},
'VaultShareKeychain'
);

export const VaultShareData = t.intersection(
[
t.type({
id: t.string,
enterpriseId: t.string,
vaultId: t.string,
fromUser: t.string,
toUser: t.string,
permissions: t.array(VaultPermission),
state: VaultShareState,
createdAt: DateFromISOString,
}),
t.partial({
vaultLabel: t.string,
message: t.string,
pendingApprovalId: t.string,
isUMSInitiated: t.boolean,
keychains: t.array(VaultShareKeychain),
updatedAt: DateFromISOString,
}),
],
'VaultShareData'
);

// ---- request bodies ----

/** POST /enterprise/:eId/vaults — Phase 1 carries no key material. */
export const InitializeVaultBody = t.type({ label: t.string }, 'InitializeVaultBody');

/** POST /enterprise/:eId/vaults/:vId/finalize — the 12 key ids as 4 ordered triplets. */
export const FinalizeVaultBody = t.type({ rootKeys: VaultRootKeys }, 'FinalizeVaultBody');

/** POST/DELETE /enterprise/:eId/vaults/:vId/freeze */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/** POST/DELETE /enterprise/:eId/vaults/:vId/freeze */
/** POST /enterprise/:eId/vaults/:vId/freeze */

export const FreezeVaultBody = t.partial({ duration: t.number }, 'FreezeVaultBody');
100 changes: 100 additions & 0 deletions modules/sdk-core/src/bitgo/vault/iVault.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* @prettier
*
* @experimental The vault client surface is experimental and may change (including breaking
* changes) before the public release.
*/
import * as t from 'io-ts';
import type { FreezeOptions, Wallet, WalletShare } from '../wallet';
import * as VaultCodecs from './codecs';

// ---- data shapes (derived from the io-ts codecs in ./codecs — single source of truth) ----

export type RootKeyType = t.TypeOf<typeof VaultCodecs.RootKeyType>;
export type VaultPermission = t.TypeOf<typeof VaultCodecs.VaultPermission>;
export type VaultRootKeys = t.TypeOf<typeof VaultCodecs.VaultRootKeys>;
export type VaultMembershipData = t.TypeOf<typeof VaultCodecs.VaultMembershipData>;
export type VaultShareRequest = t.TypeOf<typeof VaultCodecs.VaultShareRequest>;
export type VaultData = t.TypeOf<typeof VaultCodecs.VaultData>;
export type VaultShareState = t.TypeOf<typeof VaultCodecs.VaultShareState>;
export type VaultShareKeychain = t.TypeOf<typeof VaultCodecs.VaultShareKeychain>;
export type VaultShareData = t.TypeOf<typeof VaultCodecs.VaultShareData>;

export interface InitializeVaultOptions {
label: string;
}
Comment thread
davidkaplanbitgo marked this conversation as resolved.

// Phase 3 — the client hands back the 12 key ids it created in Phase 2:
export interface FinalizeVaultOptions {
rootKeys: VaultRootKeys;
}

/**
* Sharing ONE vault wallet with a non-member rides the existing wallet-share handshake (FR-13),
* so the result is the existing WalletShare shape.
*/
export type WalletShareData = WalletShare;

// ---- per-vault operation options (bodies land in WCN-1203 / WCN-1204) ----

export interface CreateVaultWalletOptions {
coin: string;
label: string;
type?: string;
multisigTypeVersion?: string;
}

interface AddVaultMemberBase {
permissions: VaultPermission[];
/** required when 'spend' is included — the 4 root user keys ECDH-re-encrypted to the invitee */
keychains?: VaultShareKeychain[];
message?: string;
/** when true, suppress the invitation email that would otherwise be sent to `email` */
disableEmail?: boolean;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a jsdoc on what this parameter does.

}

/** Add a member by either `userId` or `email` — exactly one is required. */
export type AddVaultMemberOptions =
| (AddVaultMemberBase & { userId: string; email?: never })
| (AddVaultMemberBase & { email: string; userId?: never });

export interface AddVaultWalletMemberOptions {
walletId: string;
/** required — sharing re-encrypts the user key, which needs hardened derivation from the passphrase */
walletPassphrase: string;
email?: string;
permissions?: string[];
message?: string;
}
Comment thread
davidkaplanbitgo marked this conversation as resolved.

export type AcceptVaultShareAsSpenderOptions = {
vaultShareId: string;
userPassword: string;
newWalletPassphrase?: string;
};
export type AcceptVaultShareAsNonSpenderOptions = {
vaultShareId: string;
};
export type AcceptVaultShareOptions = AcceptVaultShareAsSpenderOptions | AcceptVaultShareAsNonSpenderOptions;

/**
* @experimental
*/
export interface IVault {
Comment thread
davidkaplanbitgo marked this conversation as resolved.
id(): string;
enterpriseId(): string;
label(): string;
status(): VaultData['status'];
url(extra?: string): string;
createWallet(params: CreateVaultWalletOptions): Promise<Wallet>;
// whole-vault: view/admin/spend; spend opens a key share (also how a spender services a

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// whole-vault: view/admin/spend; spend opens a key share (also how a spender services a
// whole-vault: view/admin/spend/dapp; spend opens a key share (also how a spender services a

// vaultShareRequests entry in UMS orgs)
addMember(params: AddVaultMemberOptions): Promise<VaultData>;
// share ONE vault wallet, not the whole vault
addMemberToWallet(params: AddVaultWalletMemberOptions): Promise<WalletShareData>;
listShares(params?: { state?: VaultShareState }): Promise<VaultShareData[]>;
acceptShare(params: AcceptVaultShareOptions): Promise<VaultShareData>;
freeze(params?: FreezeOptions): Promise<VaultData>;
archive(): Promise<VaultData>;
toJSON(): VaultData;
}
60 changes: 60 additions & 0 deletions modules/sdk-core/src/bitgo/vault/iVaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* @prettier
*
* @experimental The vault client surface is experimental and may change (including breaking
* changes) before the public release.
*/
import { FinalizeVaultOptions, InitializeVaultOptions } from './iVault';
import { Vault } from './vault';

/**
* Options for vault creation.
*
* v1 targets the HOT custody model only: the multisig root user/backup keys are generated locally
* by the SDK, encrypted with `passphrase`, and registered on BitGo; the MPC roots run the standard
* hot ceremonies with the same passphrase. Self-managed cold keys and custodial vaults are out of
* scope for v1.
*/
export interface CreateVaultOptions {
label: string;
passphrase: string; // encrypts the locally-generated multisig user/backup prvs; shared with the MPC ceremonies
}

/** Handle returned by `initializeVault`, threaded into the key ceremonies and finalize. */
export interface VaultCreationHandle {
vaultId: string;
}

/**
* The 12 minted root key ids produced by `createVaultKeys`, as 4 ordered [user, backup, bitgo]
* triplets — exactly the payload `finalizeVault` consumes.
*/
export type VaultKeys = FinalizeVaultOptions;

export interface ListVaultsOptions {
cursor?: string; // opaque cursor from a previous response's nextCursor
limit?: number;
}

export interface GetVaultOptions {
id: string;
}

/**
* @experimental
*/
export interface IVaults {
/**
* One-call convenience wrapper: initialize → createVaultKeys (4 vaultId-tagged ceremonies) →
* finalize → keycard. HOT custody only in v1.
*/
generateVault(params: CreateVaultOptions): Promise<Vault>;
/** Phase 1 — initialize a vault (metadata only, no key material). */
initializeVault(params: InitializeVaultOptions): Promise<Vault>;
/** Phase 2 — run the 4 root key ceremonies tagged with `vaultId`; returns the 12 minted key ids. */
createVaultKeys(params: CreateVaultOptions & VaultCreationHandle): Promise<VaultKeys>;
/** Phase 3 — finalize a vault with the 12 root key ids. Idempotent. */
finalizeVault(vaultId: string, params: FinalizeVaultOptions): Promise<Vault>;
list(params?: ListVaultsOptions): Promise<{ vaults: Vault[]; nextCursor?: string }>;
get(params: GetVaultOptions): Promise<Vault>;
}
4 changes: 4 additions & 0 deletions modules/sdk-core/src/bitgo/vault/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './iVault';
export * from './iVaults';
export * from './vault';
export * from './vaults';
Loading