Skip to content
Closed
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
4 changes: 1 addition & 3 deletions modules/express/encryptedPrivKeys.json
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
{
"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=\"}"
}
{}
24 changes: 19 additions & 5 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ import { isLightningCoinName } from '@bitgo/abstract-lightning';
import { handleLightningWithdraw } from './lightning/lightningWithdrawRoutes';
import createExpressRouter from './typedRoutes';
import { ExpressApiRouteRequest } from './typedRoutes/api';
import { CreateTSSSendParamsBody } from './typedRoutes/api/common/createTSSSendParams';
import { createValidationError } from './typedRoutes/utils';
import { TypedRequestHandler, WrappedRequest, WrappedResponse } from '@api-ts/typed-express-router';

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

function createTSSSendParams(req: express.Request, wallet: Wallet) {
// Return type is intentionally `any` to preserve the pre-existing behaviour of
// the three call sites (wallet.sendMany / sendAccountConsolidations /
// ensureCleanSigSharesAndSignTransaction each take slightly different option
// shapes). This ticket only narrows the *input* via the io-ts codec; return
// type strengthening is tracked separately.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function createTSSSendParams(req: express.Request, wallet: Wallet): any {
const decoded = CreateTSSSendParamsBody.decode(req.body);
if (decoded._tag === 'Left') {
throw createValidationError(decoded.left);
}
const body = decoded.right;

if (req.config?.externalSignerUrl !== undefined) {
const coin = req.bitgo.coin(req.params.coin);
if (coin.getMPCAlgorithm() === MPCType.EDDSA) {
return {
...req.body,
...body,
customCommitmentGeneratingFunction: createCustomCommitmentGenerator(
req.config.externalSignerUrl,
req.params.coin
Expand All @@ -893,7 +907,7 @@ function createTSSSendParams(req: express.Request, wallet: Wallet) {
} else if (coin.getMPCAlgorithm() === MPCType.ECDSA) {
if (wallet._wallet.multisigTypeVersion === 'MPCv2') {
return {
...req.body,
...body,
customMPCv2SigningRound1GenerationFunction: createCustomMPCv2SigningRound1Generator(
req.config.externalSignerUrl,
req.params.coin
Expand All @@ -909,7 +923,7 @@ function createTSSSendParams(req: express.Request, wallet: Wallet) {
};
} else {
return {
...req.body,
...body,
customPaillierModulusGeneratingFunction: createCustomPaillierModulusGetter(
req.config.externalSignerUrl,
req.params.coin
Expand All @@ -926,7 +940,7 @@ function createTSSSendParams(req: express.Request, wallet: Wallet) {
throw new Error(`MPC Algorithm ${coin.getMPCAlgorithm()} is not supported.`);
}
} else {
return req.body;
return body;
}
}

Expand Down
161 changes: 161 additions & 0 deletions modules/express/src/typedRoutes/api/common/createTSSSendParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/**
* @prettier
*/
import * as t from 'io-ts';
import { EIP1559Params, MemoParams, TokenEnablement } from '../v2/sendmany';

/**
* Recipient entry accepted by the sendMany / sendCoins / consolidateAccount
* flows that feed `createTSSSendParams`.
*/
const Recipient = t.intersection([
t.type({
address: t.string,
amount: t.union([t.number, t.string]),
}),
t.partial({
feeLimit: t.string,
tokenName: t.string,
// `data` and `tokenData` are passed through to wallet.send*; their strict
// shapes live in the route-level codecs. We accept `any` here to mirror
// that precedent and preserve existing behaviour.
data: t.any,
tokenData: t.any,
memo: t.union([t.string, MemoParams]),
}),
]);

/**
* Request body accepted by `createTSSSendParams` in clientRoutes.
*
* The helper is called from three route handlers, all of which already
* validate their own request bodies at the route layer:
* - express.wallet.signtxtss (walletTxSignTSS)
* - express.wallet.consolidateaccount (consolidateAccount)
* - express.wallet.sendmany / express.wallet.sendcoins (sendmany / sendCoins)
*
* This codec is the helper's own contract: the union of fields any of those
* callers may place into `req.body` before it is spread into the TSS signing
* params. Every field is optional because each caller only uses a subset.
*
* Unknown fields are preserved by io-ts `t.partial` decoding, so extra
* coin-specific or forward-compatible fields flow through unchanged.
*/
export const CreateTSSSendParamsBody = t.partial({
// Auth / signing material
walletPassphrase: t.string,
xprv: t.string,
prv: t.string,
pubs: t.array(t.string),
cosignerPub: t.string,
isLastSignature: t.boolean,
otp: t.string,
derivationSeed: t.string,

// TSS / transaction request / prebuild
txRequestId: t.string,
// `txPrebuild` / `prebuildTx` / verification blobs are passed through to the
// wallet SDK, which owns their strict shapes. Mirrors the route-level
// codecs (see sendmany / consolidateAccount) which also use `t.any`.
txPrebuild: t.any,
prebuildTx: t.union([t.string, t.any]),
apiVersion: t.string,
multisigTypeVersion: t.literal('MPCv2'),
signingStep: t.union([t.literal('signerNonce'), t.literal('signerSignature'), t.literal('cosignerNonce')]),

// Recipients / addresses / amounts
recipients: t.array(Recipient),
address: t.string,
amount: t.union([t.number, t.string]),
messages: t.array(
t.type({
address: t.string,
message: t.string,
})
),
senderAddress: t.string,
senderWalletId: t.string,
receiveAddress: t.string,
changeAddress: t.string,
closeRemainderTo: t.string,
consolidateAddresses: t.array(t.string),

// Fees / gas
feeRate: t.union([t.number, t.string]),
feeLimit: t.string,
feeMultiplier: t.number,
maxFeeRate: t.number,
numBlocks: t.number,
gasLimit: t.union([t.string, t.number]),
gasPrice: t.union([t.string, t.number]),
eip1559: EIP1559Params,

// Unspents / confirmation policy
minConfirms: t.number,
enforceMinConfirmsForChange: t.boolean,
targetWalletUnspents: t.number,
minValue: t.union([t.number, t.string]),
maxValue: t.union([t.number, t.string]),
noSplitChange: t.boolean,
unspents: t.array(t.string),
allowExternalChangeAddress: t.boolean,
allowNonSegwitSigningWithoutPrevTx: t.boolean,

// Metadata / tracking
sequenceId: t.union([t.string, t.number]),
comment: t.string,
message: t.string,
memo: MemoParams,
transferId: t.number,
custodianTransactionId: t.string,
tokenName: t.string,
type: t.string,
addressType: t.string,
changeAddressType: t.string,
txFormat: t.union([t.literal('legacy'), t.literal('psbt'), t.literal('psbt-lite')]),
keepAlive: t.boolean,
instant: t.boolean,
hop: t.boolean,
isTss: t.boolean,
isTestTransaction: t.boolean,
isEvmBasedCrossChainRecovery: t.boolean,
preview: t.boolean,
offlineVerification: t.boolean,
data: t.string,
expireTime: t.number,

// Ledger / block validity windows
lastLedgerSequence: t.number,
ledgerSequenceDelta: t.number,
validFromBlock: t.number,
validToBlock: t.number,

// Token / NFT / enablement
nftCollectionId: t.string,
nftId: t.string,
enableTokens: t.array(TokenEnablement),

// Misc account-based / enterprise fields
nonce: t.string,
nonParticipation: t.boolean,
walletContractAddress: t.string,
idfSignedTimestamp: t.string,
idfUserId: t.string,
idfVersion: t.number,
lowFeeTxid: t.string,
reservation: t.partial({
expireTime: t.string,
pendingApprovalId: t.string,
}),

// Verification
verifyTxParams: t.any,
verification: t.any,

// Chain-specific opaque blobs (validated at the route layer when present)
solInstructions: t.any,
solVersionedTransactionData: t.any,
aptosCustomTransactionParams: t.any,
});

export type CreateTSSSendParamsBody = t.TypeOf<typeof CreateTSSSendParamsBody>;
27 changes: 27 additions & 0 deletions modules/express/src/typedRoutes/api/v1/verifyAddress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as t from 'io-ts';
import { httpRoute, httpRequest } from '@api-ts/io-ts-http';
import { BitgoExpressError } from '../../schemas/error';

export const VerifyAddressBody = {
address: t.string,
};

/**
* Verify Address (v1)
*
* @operationId express.v1.verifyaddress
* @tag express
*/
export const PostV1VerifyAddress = httpRoute({
path: '/api/v1/verifyaddress',
method: 'POST',
request: httpRequest({
body: VerifyAddressBody,
}),
response: {
200: t.type({
verified: t.boolean,
}),
404: BitgoExpressError,
},
});
Loading
Loading