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
8 changes: 5 additions & 3 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1005,9 +1005,11 @@ export async function handleV2EnableTokens(req: express.Request) {
* Handle Update Wallet
* @param req
*/
async function handleWalletUpdate(req: express.Request): Promise<unknown> {
export async function handleWalletUpdate(
req: ExpressApiRouteRequest<'express.wallet.update', 'put'>
): Promise<unknown> {
// If it's a lightning coin, use the lightning-specific handler
if (isLightningCoinName(req.params.coin)) {
if (isLightningCoinName(req.decoded.coin)) {
return handleUpdateLightningWalletCoinSpecific(req);
}

Expand Down Expand Up @@ -1607,7 +1609,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
// generate wallet
app.post('/api/v2/:coin/wallet/generate', parseBody, prepareBitGo(config), promiseWrapper(handleV2GenerateWallet));

app.put('/express/api/v2/:coin/wallet/:id', parseBody, prepareBitGo(config), promiseWrapper(handleWalletUpdate));
router.put('express.wallet.update', [prepareBitGo(config), typedPromiseWrapper(handleWalletUpdate)]);

// change wallet passphrase
app.post(
Expand Down
26 changes: 8 additions & 18 deletions modules/express/src/lightning/lightningWalletRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
import * as express from 'express';
import { ApiResponseError } from '../errors';
import { UpdateLightningWalletClientRequest, updateWalletCoinSpecific } from '@bitgo/abstract-lightning';
import { decodeOrElse } from '@bitgo/sdk-core';
import { updateWalletCoinSpecific } from '@bitgo/abstract-lightning';
import { ExpressApiRouteRequest } from '../typedRoutes/api';

export async function handleUpdateLightningWalletCoinSpecific(req: express.Request): Promise<unknown> {
export async function handleUpdateLightningWalletCoinSpecific(
req: ExpressApiRouteRequest<'express.wallet.update', 'put'>
): Promise<unknown> {
const bitgo = req.bitgo;

const params = decodeOrElse(
'UpdateLightningWalletClientRequest',
UpdateLightningWalletClientRequest,
req.body,
(_) => {
// DON'T throw errors from decodeOrElse. It could leak sensitive information.
throw new ApiResponseError('Invalid request body to update lightning wallet coin specific', 400);
}
);
const coin = bitgo.coin(req.decoded.coin);
const wallet = await coin.wallets().get({ id: req.decoded.id, includeBalance: false });

const coin = bitgo.coin(req.params.coin);
const wallet = await coin.wallets().get({ id: req.params.id, includeBalance: false });

return await updateWalletCoinSpecific(wallet, params);
return await updateWalletCoinSpecific(wallet, req.decoded);
}
4 changes: 4 additions & 0 deletions modules/express/src/typedRoutes/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { PostCoinSignTx } from './v2/coinSignTx';
import { PostWalletSignTx } from './v2/walletSignTx';
import { PostWalletTxSignTSS } from './v2/walletTxSignTSS';
import { PostShareWallet } from './v2/shareWallet';
import { PutExpressWalletUpdate } from './v2/expressWalletUpdate';

export const ExpressApi = apiSpec({
'express.ping': {
Expand Down Expand Up @@ -116,6 +117,9 @@ export const ExpressApi = apiSpec({
'express.v2.wallet.share': {
post: PostShareWallet,
},
'express.wallet.update': {
put: PutExpressWalletUpdate,
},
});

export type ExpressApi = typeof ExpressApi;
Expand Down
59 changes: 59 additions & 0 deletions modules/express/src/typedRoutes/api/v2/expressWalletUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as t from 'io-ts';
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
import { BitgoExpressError } from '../../schemas/error';
import { WalletResponse } from '../../schemas/wallet';

/**
* Parameters for Express Wallet Update
*/
export const ExpressWalletUpdateParams = {
/** Coin ticker / chain identifier */
coin: t.string,
/** Wallet ID */
id: t.string,
} as const;

/**
* Request body for Express Wallet Update
*/
export const ExpressWalletUpdateBody = {
/** The host address of the lightning signer node. */
signerHost: t.string,
/** The TLS certificate for the lighting signer node encoded to base64. */
signerTlsCert: t.string,
/** (Optional) The signer macaroon for the lighting signer node. */
signerMacaroon: optional(t.string),
/** The wallet passphrase (used locally to decrypt and sign). */
passphrase: t.string,
} as const;

/**
* Response for Express Wallet Update
*/
export const ExpressWalletUpdateResponse = {
/** Updated Wallet - Returns the wallet with updated Lightning signer configuration */
200: WalletResponse,
/** Bad Request - Invalid parameters or missing required fields */
400: BitgoExpressError,
/** Forbidden - Insufficient permissions to update the wallet */
403: BitgoExpressError,
/** Not Found - Wallet not found or invalid coin type */
404: BitgoExpressError,
} as const;

/**
* Express - Update Wallet
* The express update wallet route is meant to be used for lightning (lnbtc/tlnbtc).
* For other coins, use the standard wallet update endpoint.
*
* @operationId express.wallet.update
*/
export const PutExpressWalletUpdate = httpRoute({
path: '/express/api/v2/{coin}/wallet/{id}',
method: 'PUT',
request: httpRequest({
params: ExpressWalletUpdateParams,
body: ExpressWalletUpdateBody,
}),
response: ExpressWalletUpdateResponse,
});
225 changes: 225 additions & 0 deletions modules/express/src/typedRoutes/schemas/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,228 @@ export const ShareWalletKeychain = t.partial({
toPubKey: t.string,
path: t.string,
});

/**
* Wallet user with permissions
*/
export const WalletUser = t.type({
user: t.string,
permissions: t.array(t.string),
});

/**
* Address balance information
*/
export const AddressBalance = t.type({
updated: t.string,
balance: t.number,
balanceString: t.string,
totalReceived: t.number,
totalSent: t.number,
confirmedBalanceString: t.string,
spendableBalanceString: t.string,
});

/**
* Address information
*/
export const ReceiveAddress = t.partial({
/** Address ID */
id: t.string,
/** The actual address string */
address: t.string,
/** Chain index (0 for external, 1 for internal) */
chain: t.number,
/** Address index */
index: t.number,
/** Coin type */
coin: t.string,
/** Wallet ID this address belongs to */
wallet: t.string,
/** Last nonce used */
lastNonce: t.number,
/** Coin-specific address data */
coinSpecific: t.UnknownRecord,
/** Address balance information */
balance: AddressBalance,
/** Address label */
label: t.string,
/** Address type (e.g., 'p2sh', 'p2wsh') */
addressType: t.string,
});

/**
* Policy rule for wallet
*/
export const PolicyRule = t.partial({
/** Rule ID */
id: t.string,
/** Rule type */
type: t.string,
/** Date when rule becomes locked */
lockDate: t.string,
/** Mutability constraint */
mutabilityConstraint: t.string,
/** Coin this rule applies to */
coin: t.string,
/** Rule condition */
condition: t.UnknownRecord,
/** Rule action */
action: t.UnknownRecord,
});

/**
* Wallet policy
*/
export const WalletPolicy = t.partial({
/** Policy ID */
id: t.string,
/** Policy creation date */
date: t.string,
/** Policy version number */
version: t.number,
/** Policy label */
label: t.string,
/** Whether this is the latest version */
latest: t.boolean,
/** Policy rules */
rules: t.array(PolicyRule),
});

/**
* Admin settings for wallet
*/
export const WalletAdmin = t.partial({
policy: WalletPolicy,
});

/**
* Freeze information
*/
export const WalletFreeze = t.partial({
time: t.string,
expires: t.string,
});

/**
* Build defaults for wallet transactions
*/
export const BuildDefaults = t.partial({
minFeeRate: t.number,
maxFeeRate: t.number,
feeMultiplier: t.number,
changeAddressType: t.string,
txFormat: t.string,
});

/**
* Custom change key signatures
*/
export const CustomChangeKeySignatures = t.partial({
user: t.string,
backup: t.string,
bitgo: t.string,
});

/**
* Wallet response data
* Comprehensive wallet information returned from wallet operations
* Based on WalletData interface from sdk-core
*/
export const WalletResponse = t.partial({
/** Wallet ID */
id: t.string,
/** Wallet label/name */
label: t.string,
/** Coin type (e.g., btc, tlnbtc, lnbtc) */
coin: t.string,
/** Array of keychain IDs */
keys: t.array(t.string),
/** Number of signatures required (m in m-of-n) */
m: t.number,
/** Total number of keys (n in m-of-n) */
n: t.number,
/** Number of approvals required for transactions */
approvalsRequired: t.number,
/** Wallet balance as number */
balance: t.number,
/** Confirmed balance as number */
confirmedBalance: t.number,
/** Spendable balance as number */
spendableBalance: t.number,
/** Wallet balance as string */
balanceString: t.string,
/** Confirmed balance as string */
confirmedBalanceString: t.string,
/** Spendable balance as string */
spendableBalanceString: t.string,
/** Number of unspent outputs */
unspentCount: t.number,
/** Enterprise ID this wallet belongs to */
enterprise: t.string,
/** Wallet type (e.g., 'hot', 'cold', 'custodial') */
type: t.string,
/** Wallet subtype (e.g., 'lightningSelfCustody') */
subType: t.string,
/** Multisig type ('onchain' or 'tss') */
multisigType: t.union([t.literal('onchain'), t.literal('tss')]),
/** Multisig type version (e.g., 'MPCv2') */
multisigTypeVersion: t.string,
/** Coin-specific wallet data */
coinSpecific: t.UnknownRecord,
/** Admin settings including policy */
admin: WalletAdmin,
/** Users with access to this wallet */
users: t.array(WalletUser),
/** Receive address information */
receiveAddress: ReceiveAddress,
/** Whether the wallet can be recovered */
recoverable: t.boolean,
/** Tags associated with the wallet */
tags: t.array(t.string),
/** Whether backup key signing is allowed */
allowBackupKeySigning: t.boolean,
/** Build defaults for transactions */
buildDefaults: BuildDefaults,
/** Whether the wallet is cold storage */
isCold: t.boolean,
/** Custodial wallet information */
custodialWallet: t.UnknownRecord,
/** Custodial wallet ID */
custodialWalletId: t.string,
/** Whether the wallet is deleted */
deleted: t.boolean,
/** Whether transaction notifications are disabled */
disableTransactionNotifications: t.boolean,
/** Freeze status */
freeze: WalletFreeze,
/** Node ID for lightning wallets */
nodeId: t.string,
/** Pending approvals for this wallet */
pendingApprovals: t.array(t.UnknownRecord),
/** Start date information */
startDate: t.UnknownRecord,
/** Custom change key signatures */
customChangeKeySignatures: CustomChangeKeySignatures,
/** Wallet which this was migrated from */
migratedFrom: t.string,
/** EVM keyring reference wallet ID */
evmKeyRingReferenceWalletId: t.string,
/** Whether this is a parent wallet */
isParent: t.boolean,
/** Enabled child chains */
enabledChildChains: t.array(t.string),
/** Wallet flags */
walletFlags: t.array(
t.type({
name: t.string,
value: t.string,
})
),
/** Token balances */
tokens: t.array(t.UnknownRecord),
/** NFT balances */
nfts: t.record(t.string, t.UnknownRecord),
/** Unsupported NFT balances */
unsupportedNfts: t.record(t.string, t.UnknownRecord),
});
Loading