Skip to content

Commit 910a4a0

Browse files
feat(express): add Type codec for createTSSSendParams
TICKET: WCI-48
1 parent 6d653da commit 910a4a0

5 files changed

Lines changed: 353 additions & 8 deletions

File tree

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
{
2-
"61f039aad587c2000745c687373e0fa9": "{\"iv\":\"/Gnh+Ip1G+IOhy+Cms+umQ==\",\"v\":1,\"iter\":10000,\"ks\":256,\"ts\":64,\"mode\":\"ccm\",\"adata\":\"\",\"cipher\":\"aes\",\"salt\":\"FYnd1xwReTw=\",\"ct\":\"vgnCvdJ1Z9sqeV6urYxNsscwnkB/6eSPsZhzaW4Cuc7RKEY1uWNlleR0Tjtd8nlQuhsA5UXFpOID3lHHHjPDvB+jWtRm08I2F+HNGYuklWG12vIiSrY2KnkYRJkyCghn5Pq3iEimQb9M2kkwj5wf4EtfAiz9jsY=\"}"
3-
}
1+
{}

modules/express/src/clientRoutes.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ import { isLightningCoinName } from '@bitgo/abstract-lightning';
6161
import { handleLightningWithdraw } from './lightning/lightningWithdrawRoutes';
6262
import createExpressRouter from './typedRoutes';
6363
import { ExpressApiRouteRequest } from './typedRoutes/api';
64+
import { CreateTSSSendParamsBody } from './typedRoutes/api/common/createTSSSendParams';
65+
import { createValidationError } from './typedRoutes/utils';
6466
import { TypedRequestHandler, WrappedRequest, WrappedResponse } from '@api-ts/typed-express-router';
6567

6668
const { version } = require('bitgo/package.json');
@@ -877,12 +879,24 @@ function createSendParams(req: express.Request) {
877879
}
878880
}
879881

880-
function createTSSSendParams(req: express.Request, wallet: Wallet) {
882+
// Return type is intentionally `any` to preserve the pre-existing behaviour of
883+
// the three call sites (wallet.sendMany / sendAccountConsolidations /
884+
// ensureCleanSigSharesAndSignTransaction each take slightly different option
885+
// shapes). This ticket only narrows the *input* via the io-ts codec; return
886+
// type strengthening is tracked separately.
887+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
888+
export function createTSSSendParams(req: express.Request, wallet: Wallet): any {
889+
const decoded = CreateTSSSendParamsBody.decode(req.body);
890+
if (decoded._tag === 'Left') {
891+
throw createValidationError(decoded.left);
892+
}
893+
const body = decoded.right;
894+
881895
if (req.config?.externalSignerUrl !== undefined) {
882896
const coin = req.bitgo.coin(req.params.coin);
883897
if (coin.getMPCAlgorithm() === MPCType.EDDSA) {
884898
return {
885-
...req.body,
899+
...body,
886900
customCommitmentGeneratingFunction: createCustomCommitmentGenerator(
887901
req.config.externalSignerUrl,
888902
req.params.coin
@@ -893,7 +907,7 @@ function createTSSSendParams(req: express.Request, wallet: Wallet) {
893907
} else if (coin.getMPCAlgorithm() === MPCType.ECDSA) {
894908
if (wallet._wallet.multisigTypeVersion === 'MPCv2') {
895909
return {
896-
...req.body,
910+
...body,
897911
customMPCv2SigningRound1GenerationFunction: createCustomMPCv2SigningRound1Generator(
898912
req.config.externalSignerUrl,
899913
req.params.coin
@@ -909,7 +923,7 @@ function createTSSSendParams(req: express.Request, wallet: Wallet) {
909923
};
910924
} else {
911925
return {
912-
...req.body,
926+
...body,
913927
customPaillierModulusGeneratingFunction: createCustomPaillierModulusGetter(
914928
req.config.externalSignerUrl,
915929
req.params.coin
@@ -926,7 +940,7 @@ function createTSSSendParams(req: express.Request, wallet: Wallet) {
926940
throw new Error(`MPC Algorithm ${coin.getMPCAlgorithm()} is not supported.`);
927941
}
928942
} else {
929-
return req.body;
943+
return body;
930944
}
931945
}
932946

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/**
2+
* @prettier
3+
*/
4+
import * as t from 'io-ts';
5+
import { EIP1559Params, MemoParams, TokenEnablement } from '../v2/sendmany';
6+
7+
/**
8+
* Recipient entry accepted by the sendMany / sendCoins / consolidateAccount
9+
* flows that feed `createTSSSendParams`.
10+
*/
11+
const Recipient = t.intersection([
12+
t.type({
13+
address: t.string,
14+
amount: t.union([t.number, t.string]),
15+
}),
16+
t.partial({
17+
feeLimit: t.string,
18+
tokenName: t.string,
19+
// `data` and `tokenData` are passed through to wallet.send*; their strict
20+
// shapes live in the route-level codecs. We accept `any` here to mirror
21+
// that precedent and preserve existing behaviour.
22+
data: t.any,
23+
tokenData: t.any,
24+
memo: t.union([t.string, MemoParams]),
25+
}),
26+
]);
27+
28+
/**
29+
* Request body accepted by `createTSSSendParams` in clientRoutes.
30+
*
31+
* The helper is called from three route handlers, all of which already
32+
* validate their own request bodies at the route layer:
33+
* - express.wallet.signtxtss (walletTxSignTSS)
34+
* - express.wallet.consolidateaccount (consolidateAccount)
35+
* - express.wallet.sendmany / express.wallet.sendcoins (sendmany / sendCoins)
36+
*
37+
* This codec is the helper's own contract: the union of fields any of those
38+
* callers may place into `req.body` before it is spread into the TSS signing
39+
* params. Every field is optional because each caller only uses a subset.
40+
*
41+
* Unknown fields are preserved by io-ts `t.partial` decoding, so extra
42+
* coin-specific or forward-compatible fields flow through unchanged.
43+
*/
44+
export const CreateTSSSendParamsBody = t.partial({
45+
// Auth / signing material
46+
walletPassphrase: t.string,
47+
xprv: t.string,
48+
prv: t.string,
49+
pubs: t.array(t.string),
50+
cosignerPub: t.string,
51+
isLastSignature: t.boolean,
52+
otp: t.string,
53+
derivationSeed: t.string,
54+
55+
// TSS / transaction request / prebuild
56+
txRequestId: t.string,
57+
// `txPrebuild` / `prebuildTx` / verification blobs are passed through to the
58+
// wallet SDK, which owns their strict shapes. Mirrors the route-level
59+
// codecs (see sendmany / consolidateAccount) which also use `t.any`.
60+
txPrebuild: t.any,
61+
prebuildTx: t.union([t.string, t.any]),
62+
apiVersion: t.string,
63+
multisigTypeVersion: t.literal('MPCv2'),
64+
signingStep: t.union([t.literal('signerNonce'), t.literal('signerSignature'), t.literal('cosignerNonce')]),
65+
66+
// Recipients / addresses / amounts
67+
recipients: t.array(Recipient),
68+
address: t.string,
69+
amount: t.union([t.number, t.string]),
70+
messages: t.array(
71+
t.type({
72+
address: t.string,
73+
message: t.string,
74+
})
75+
),
76+
senderAddress: t.string,
77+
senderWalletId: t.string,
78+
receiveAddress: t.string,
79+
changeAddress: t.string,
80+
closeRemainderTo: t.string,
81+
consolidateAddresses: t.array(t.string),
82+
83+
// Fees / gas
84+
feeRate: t.union([t.number, t.string]),
85+
feeLimit: t.string,
86+
feeMultiplier: t.number,
87+
maxFeeRate: t.number,
88+
numBlocks: t.number,
89+
gasLimit: t.union([t.string, t.number]),
90+
gasPrice: t.union([t.string, t.number]),
91+
eip1559: EIP1559Params,
92+
93+
// Unspents / confirmation policy
94+
minConfirms: t.number,
95+
enforceMinConfirmsForChange: t.boolean,
96+
targetWalletUnspents: t.number,
97+
minValue: t.union([t.number, t.string]),
98+
maxValue: t.union([t.number, t.string]),
99+
noSplitChange: t.boolean,
100+
unspents: t.array(t.string),
101+
allowExternalChangeAddress: t.boolean,
102+
allowNonSegwitSigningWithoutPrevTx: t.boolean,
103+
104+
// Metadata / tracking
105+
sequenceId: t.union([t.string, t.number]),
106+
comment: t.string,
107+
message: t.string,
108+
memo: MemoParams,
109+
transferId: t.number,
110+
custodianTransactionId: t.string,
111+
tokenName: t.string,
112+
type: t.string,
113+
addressType: t.string,
114+
changeAddressType: t.string,
115+
txFormat: t.union([t.literal('legacy'), t.literal('psbt'), t.literal('psbt-lite')]),
116+
keepAlive: t.boolean,
117+
instant: t.boolean,
118+
hop: t.boolean,
119+
isTss: t.boolean,
120+
isTestTransaction: t.boolean,
121+
isEvmBasedCrossChainRecovery: t.boolean,
122+
preview: t.boolean,
123+
offlineVerification: t.boolean,
124+
data: t.string,
125+
expireTime: t.number,
126+
127+
// Ledger / block validity windows
128+
lastLedgerSequence: t.number,
129+
ledgerSequenceDelta: t.number,
130+
validFromBlock: t.number,
131+
validToBlock: t.number,
132+
133+
// Token / NFT / enablement
134+
nftCollectionId: t.string,
135+
nftId: t.string,
136+
enableTokens: t.array(TokenEnablement),
137+
138+
// Misc account-based / enterprise fields
139+
nonce: t.string,
140+
nonParticipation: t.boolean,
141+
walletContractAddress: t.string,
142+
idfSignedTimestamp: t.string,
143+
idfUserId: t.string,
144+
idfVersion: t.number,
145+
lowFeeTxid: t.string,
146+
reservation: t.partial({
147+
expireTime: t.string,
148+
pendingApprovalId: t.string,
149+
}),
150+
151+
// Verification
152+
verifyTxParams: t.any,
153+
verification: t.any,
154+
155+
// Chain-specific opaque blobs (validated at the route layer when present)
156+
solInstructions: t.any,
157+
solVersionedTransactionData: t.any,
158+
aptosCustomTransactionParams: t.any,
159+
});
160+
161+
export type CreateTSSSendParamsBody = t.TypeOf<typeof CreateTSSSendParamsBody>;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as t from 'io-ts';
2+
import { httpRoute, httpRequest } from '@api-ts/io-ts-http';
3+
import { BitgoExpressError } from '../../schemas/error';
4+
5+
export const VerifyAddressBody = {
6+
address: t.string,
7+
};
8+
9+
/**
10+
* Verify Address (v1)
11+
*
12+
* @operationId express.v1.verifyaddress
13+
* @tag express
14+
*/
15+
export const PostV1VerifyAddress = httpRoute({
16+
path: '/api/v1/verifyaddress',
17+
method: 'POST',
18+
request: httpRequest({
19+
body: VerifyAddressBody,
20+
}),
21+
response: {
22+
200: t.type({
23+
verified: t.boolean,
24+
}),
25+
404: BitgoExpressError,
26+
},
27+
});

0 commit comments

Comments
 (0)