Skip to content

Commit 47b5f34

Browse files
feat(express): migrated update express wallet to typed routes
2 parents e11c733 + e4f3b4c commit 47b5f34

File tree

9 files changed

+878
-75
lines changed

9 files changed

+878
-75
lines changed

modules/express/src/clientRoutes.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,9 +1005,11 @@ export async function handleV2EnableTokens(req: express.Request) {
10051005
* Handle Update Wallet
10061006
* @param req
10071007
*/
1008-
async function handleWalletUpdate(req: express.Request): Promise<unknown> {
1008+
export async function handleWalletUpdate(
1009+
req: ExpressApiRouteRequest<'express.wallet.update', 'put'>
1010+
): Promise<unknown> {
10091011
// If it's a lightning coin, use the lightning-specific handler
1010-
if (isLightningCoinName(req.params.coin)) {
1012+
if (isLightningCoinName(req.decoded.coin)) {
10111013
return handleUpdateLightningWalletCoinSpecific(req);
10121014
}
10131015

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

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

16121614
// change wallet passphrase
16131615
app.post(
Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,13 @@
1-
import * as express from 'express';
2-
import { ApiResponseError } from '../errors';
3-
import { UpdateLightningWalletClientRequest, updateWalletCoinSpecific } from '@bitgo/abstract-lightning';
4-
import { decodeOrElse } from '@bitgo/sdk-core';
1+
import { updateWalletCoinSpecific } from '@bitgo/abstract-lightning';
2+
import { ExpressApiRouteRequest } from '../typedRoutes/api';
53

6-
export async function handleUpdateLightningWalletCoinSpecific(req: express.Request): Promise<unknown> {
4+
export async function handleUpdateLightningWalletCoinSpecific(
5+
req: ExpressApiRouteRequest<'express.wallet.update', 'put'>
6+
): Promise<unknown> {
77
const bitgo = req.bitgo;
88

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

19-
const coin = bitgo.coin(req.params.coin);
20-
const wallet = await coin.wallets().get({ id: req.params.id, includeBalance: false });
21-
22-
return await updateWalletCoinSpecific(wallet, params);
12+
return await updateWalletCoinSpecific(wallet, req.decoded);
2313
}

modules/express/src/typedRoutes/api/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { PostCoinSignTx } from './v2/coinSignTx';
3030
import { PostWalletSignTx } from './v2/walletSignTx';
3131
import { PostWalletTxSignTSS } from './v2/walletTxSignTSS';
3232
import { PostShareWallet } from './v2/shareWallet';
33+
import { PutExpressWalletUpdate } from './v2/expressWalletUpdate';
3334

3435
export const ExpressApi = apiSpec({
3536
'express.ping': {
@@ -116,6 +117,9 @@ export const ExpressApi = apiSpec({
116117
'express.v2.wallet.share': {
117118
post: PostShareWallet,
118119
},
120+
'express.wallet.update': {
121+
put: PutExpressWalletUpdate,
122+
},
119123
});
120124

121125
export type ExpressApi = typeof ExpressApi;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import * as t from 'io-ts';
2+
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
3+
import { BitgoExpressError } from '../../schemas/error';
4+
import { WalletResponse } from '../../schemas/wallet';
5+
6+
/**
7+
* Parameters for Express Wallet Update
8+
*/
9+
export const ExpressWalletUpdateParams = {
10+
/** Coin ticker / chain identifier */
11+
coin: t.string,
12+
/** Wallet ID */
13+
id: t.string,
14+
} as const;
15+
16+
/**
17+
* Request body for Express Wallet Update
18+
*/
19+
export const ExpressWalletUpdateBody = {
20+
/** The host address of the lightning signer node. */
21+
signerHost: t.string,
22+
/** The TLS certificate for the lighting signer node encoded to base64. */
23+
signerTlsCert: t.string,
24+
/** (Optional) The signer macaroon for the lighting signer node. */
25+
signerMacaroon: optional(t.string),
26+
/** The wallet passphrase (used locally to decrypt and sign). */
27+
passphrase: t.string,
28+
} as const;
29+
30+
/**
31+
* Response for Express Wallet Update
32+
*/
33+
export const ExpressWalletUpdateResponse = {
34+
/** Updated Wallet - Returns the wallet with updated Lightning signer configuration */
35+
200: WalletResponse,
36+
/** Bad Request - Invalid parameters or missing required fields */
37+
400: BitgoExpressError,
38+
/** Forbidden - Insufficient permissions to update the wallet */
39+
403: BitgoExpressError,
40+
/** Not Found - Wallet not found or invalid coin type */
41+
404: BitgoExpressError,
42+
} as const;
43+
44+
/**
45+
* Express - Update Wallet
46+
* The express update wallet route is meant to be used for lightning (lnbtc/tlnbtc).
47+
* For other coins, use the standard wallet update endpoint.
48+
*
49+
* @operationId express.wallet.update
50+
*/
51+
export const PutExpressWalletUpdate = httpRoute({
52+
path: '/express/api/v2/{coin}/wallet/{id}',
53+
method: 'PUT',
54+
request: httpRequest({
55+
params: ExpressWalletUpdateParams,
56+
body: ExpressWalletUpdateBody,
57+
}),
58+
response: ExpressWalletUpdateResponse,
59+
});

modules/express/src/typedRoutes/schemas/wallet.ts

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,228 @@ export const ShareWalletKeychain = t.partial({
1515
toPubKey: t.string,
1616
path: t.string,
1717
});
18+
19+
/**
20+
* Wallet user with permissions
21+
*/
22+
export const WalletUser = t.type({
23+
user: t.string,
24+
permissions: t.array(t.string),
25+
});
26+
27+
/**
28+
* Address balance information
29+
*/
30+
export const AddressBalance = t.type({
31+
updated: t.string,
32+
balance: t.number,
33+
balanceString: t.string,
34+
totalReceived: t.number,
35+
totalSent: t.number,
36+
confirmedBalanceString: t.string,
37+
spendableBalanceString: t.string,
38+
});
39+
40+
/**
41+
* Address information
42+
*/
43+
export const ReceiveAddress = t.partial({
44+
/** Address ID */
45+
id: t.string,
46+
/** The actual address string */
47+
address: t.string,
48+
/** Chain index (0 for external, 1 for internal) */
49+
chain: t.number,
50+
/** Address index */
51+
index: t.number,
52+
/** Coin type */
53+
coin: t.string,
54+
/** Wallet ID this address belongs to */
55+
wallet: t.string,
56+
/** Last nonce used */
57+
lastNonce: t.number,
58+
/** Coin-specific address data */
59+
coinSpecific: t.UnknownRecord,
60+
/** Address balance information */
61+
balance: AddressBalance,
62+
/** Address label */
63+
label: t.string,
64+
/** Address type (e.g., 'p2sh', 'p2wsh') */
65+
addressType: t.string,
66+
});
67+
68+
/**
69+
* Policy rule for wallet
70+
*/
71+
export const PolicyRule = t.partial({
72+
/** Rule ID */
73+
id: t.string,
74+
/** Rule type */
75+
type: t.string,
76+
/** Date when rule becomes locked */
77+
lockDate: t.string,
78+
/** Mutability constraint */
79+
mutabilityConstraint: t.string,
80+
/** Coin this rule applies to */
81+
coin: t.string,
82+
/** Rule condition */
83+
condition: t.UnknownRecord,
84+
/** Rule action */
85+
action: t.UnknownRecord,
86+
});
87+
88+
/**
89+
* Wallet policy
90+
*/
91+
export const WalletPolicy = t.partial({
92+
/** Policy ID */
93+
id: t.string,
94+
/** Policy creation date */
95+
date: t.string,
96+
/** Policy version number */
97+
version: t.number,
98+
/** Policy label */
99+
label: t.string,
100+
/** Whether this is the latest version */
101+
latest: t.boolean,
102+
/** Policy rules */
103+
rules: t.array(PolicyRule),
104+
});
105+
106+
/**
107+
* Admin settings for wallet
108+
*/
109+
export const WalletAdmin = t.partial({
110+
policy: WalletPolicy,
111+
});
112+
113+
/**
114+
* Freeze information
115+
*/
116+
export const WalletFreeze = t.partial({
117+
time: t.string,
118+
expires: t.string,
119+
});
120+
121+
/**
122+
* Build defaults for wallet transactions
123+
*/
124+
export const BuildDefaults = t.partial({
125+
minFeeRate: t.number,
126+
maxFeeRate: t.number,
127+
feeMultiplier: t.number,
128+
changeAddressType: t.string,
129+
txFormat: t.string,
130+
});
131+
132+
/**
133+
* Custom change key signatures
134+
*/
135+
export const CustomChangeKeySignatures = t.partial({
136+
user: t.string,
137+
backup: t.string,
138+
bitgo: t.string,
139+
});
140+
141+
/**
142+
* Wallet response data
143+
* Comprehensive wallet information returned from wallet operations
144+
* Based on WalletData interface from sdk-core
145+
*/
146+
export const WalletResponse = t.partial({
147+
/** Wallet ID */
148+
id: t.string,
149+
/** Wallet label/name */
150+
label: t.string,
151+
/** Coin type (e.g., btc, tlnbtc, lnbtc) */
152+
coin: t.string,
153+
/** Array of keychain IDs */
154+
keys: t.array(t.string),
155+
/** Number of signatures required (m in m-of-n) */
156+
m: t.number,
157+
/** Total number of keys (n in m-of-n) */
158+
n: t.number,
159+
/** Number of approvals required for transactions */
160+
approvalsRequired: t.number,
161+
/** Wallet balance as number */
162+
balance: t.number,
163+
/** Confirmed balance as number */
164+
confirmedBalance: t.number,
165+
/** Spendable balance as number */
166+
spendableBalance: t.number,
167+
/** Wallet balance as string */
168+
balanceString: t.string,
169+
/** Confirmed balance as string */
170+
confirmedBalanceString: t.string,
171+
/** Spendable balance as string */
172+
spendableBalanceString: t.string,
173+
/** Number of unspent outputs */
174+
unspentCount: t.number,
175+
/** Enterprise ID this wallet belongs to */
176+
enterprise: t.string,
177+
/** Wallet type (e.g., 'hot', 'cold', 'custodial') */
178+
type: t.string,
179+
/** Wallet subtype (e.g., 'lightningSelfCustody') */
180+
subType: t.string,
181+
/** Multisig type ('onchain' or 'tss') */
182+
multisigType: t.union([t.literal('onchain'), t.literal('tss')]),
183+
/** Multisig type version (e.g., 'MPCv2') */
184+
multisigTypeVersion: t.string,
185+
/** Coin-specific wallet data */
186+
coinSpecific: t.UnknownRecord,
187+
/** Admin settings including policy */
188+
admin: WalletAdmin,
189+
/** Users with access to this wallet */
190+
users: t.array(WalletUser),
191+
/** Receive address information */
192+
receiveAddress: ReceiveAddress,
193+
/** Whether the wallet can be recovered */
194+
recoverable: t.boolean,
195+
/** Tags associated with the wallet */
196+
tags: t.array(t.string),
197+
/** Whether backup key signing is allowed */
198+
allowBackupKeySigning: t.boolean,
199+
/** Build defaults for transactions */
200+
buildDefaults: BuildDefaults,
201+
/** Whether the wallet is cold storage */
202+
isCold: t.boolean,
203+
/** Custodial wallet information */
204+
custodialWallet: t.UnknownRecord,
205+
/** Custodial wallet ID */
206+
custodialWalletId: t.string,
207+
/** Whether the wallet is deleted */
208+
deleted: t.boolean,
209+
/** Whether transaction notifications are disabled */
210+
disableTransactionNotifications: t.boolean,
211+
/** Freeze status */
212+
freeze: WalletFreeze,
213+
/** Node ID for lightning wallets */
214+
nodeId: t.string,
215+
/** Pending approvals for this wallet */
216+
pendingApprovals: t.array(t.UnknownRecord),
217+
/** Start date information */
218+
startDate: t.UnknownRecord,
219+
/** Custom change key signatures */
220+
customChangeKeySignatures: CustomChangeKeySignatures,
221+
/** Wallet which this was migrated from */
222+
migratedFrom: t.string,
223+
/** EVM keyring reference wallet ID */
224+
evmKeyRingReferenceWalletId: t.string,
225+
/** Whether this is a parent wallet */
226+
isParent: t.boolean,
227+
/** Enabled child chains */
228+
enabledChildChains: t.array(t.string),
229+
/** Wallet flags */
230+
walletFlags: t.array(
231+
t.type({
232+
name: t.string,
233+
value: t.string,
234+
})
235+
),
236+
/** Token balances */
237+
tokens: t.array(t.UnknownRecord),
238+
/** NFT balances */
239+
nfts: t.record(t.string, t.UnknownRecord),
240+
/** Unsupported NFT balances */
241+
unsupportedNfts: t.record(t.string, t.UnknownRecord),
242+
});

0 commit comments

Comments
 (0)