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
15 changes: 6 additions & 9 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -664,11 +664,13 @@ export async function handleV2CreateAddress(req: ExpressApiRouteRequest<'express
* handle v2 approve transaction
* @param req
*/
async function handleV2PendingApproval(req: express.Request): Promise<any> {
async function handleV2PendingApproval(
req: ExpressApiRouteRequest<'express.v2.pendingapprovals', 'put'>
): Promise<any> {
const bitgo = req.bitgo;
const coin = bitgo.coin(req.params.coin);
const coin = bitgo.coin(req.decoded.coin);
const params = req.body || {};
const pendingApproval = await coin.pendingApprovals().get({ id: req.params.id });
const pendingApproval = await coin.pendingApprovals().get({ id: req.decoded.id });
if (params.state === 'approved') {
return pendingApproval.approve(params);
}
Expand Down Expand Up @@ -1686,12 +1688,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
// Miscellaneous
app.post('/api/v2/:coin/canonicaladdress', parseBody, prepareBitGo(config), promiseWrapper(handleCanonicalAddress));
router.post('express.verifycoinaddress', [prepareBitGo(config), typedPromiseWrapper(handleV2VerifyAddress)]);
app.put(
'/api/v2/:coin/pendingapprovals/:id',
parseBody,
prepareBitGo(config),
promiseWrapper(handleV2PendingApproval)
);
router.put('express.v2.pendingapprovals', [prepareBitGo(config), typedPromiseWrapper(handleV2PendingApproval)]);

// lightning - pay invoice
app.post(
Expand Down
10 changes: 7 additions & 3 deletions modules/express/src/typedRoutes/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { PostSendCoins } from './v2/sendCoins';
import { PostGenerateShareTSS } from './v2/generateShareTSS';
import { PostOfcExtSignPayload } from './v2/ofcExtSignPayload';
import { PostLightningWalletWithdraw } from './v2/lightningWithdraw';
import { PutV2PendingApproval } from './v2/pendingApproval';

// Too large types can cause the following error
//
Expand Down Expand Up @@ -109,10 +110,13 @@ export const ExpressV1WalletSimpleCreateApiSpec = apiSpec({
},
});

export const ExpressV1PendingApprovalsApiSpec = apiSpec({
export const ExpressPendingApprovalsApiSpec = apiSpec({
'express.v1.pendingapprovals': {
put: PutPendingApproval,
},
'express.v2.pendingapprovals': {
put: PutV2PendingApproval,
},
});

export const ExpressWalletSignTransactionApiSpec = apiSpec({
Expand Down Expand Up @@ -275,7 +279,7 @@ export type ExpressApi = typeof ExpressPingApiSpec &
typeof ExpressCalculateMinerFeeInfoApiSpec &
typeof ExpressV1WalletAcceptShareApiSpec &
typeof ExpressV1WalletSimpleCreateApiSpec &
typeof ExpressV1PendingApprovalsApiSpec &
typeof ExpressPendingApprovalsApiSpec &
typeof ExpressWalletSignTransactionApiSpec &
typeof ExpressV1KeychainDeriveApiSpec &
typeof ExpressV1KeychainLocalApiSpec &
Expand Down Expand Up @@ -309,7 +313,7 @@ export const ExpressApi: ExpressApi = {
...ExpressCalculateMinerFeeInfoApiSpec,
...ExpressV1WalletAcceptShareApiSpec,
...ExpressV1WalletSimpleCreateApiSpec,
...ExpressV1PendingApprovalsApiSpec,
...ExpressPendingApprovalsApiSpec,
...ExpressWalletSignTransactionApiSpec,
...ExpressV1KeychainDeriveApiSpec,
...ExpressV1KeychainLocalApiSpec,
Expand Down
153 changes: 153 additions & 0 deletions modules/express/src/typedRoutes/api/v2/pendingApproval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import * as t from 'io-ts';
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
import { BitgoExpressError } from '../../schemas/error';

/**
* Path parameters for pending approval endpoint
*/
export const PendingApprovalParams = {
/** Coin identifier (e.g., 'btc', 'eth', 'tbtc') */
coin: t.string,
/** Pending approval ID */
id: t.string,
} as const;

/**
* Request body for approving or rejecting a pending approval
*/
export const PendingApprovalRequestBody = {
/** State of the approval: 'approved' to approve, omit or 'rejected' to reject */
state: optional(t.string),
/** Wallet passphrase for decrypting user keys during transaction signing */
walletPassphrase: optional(t.string),
/** One-time password for 2FA verification */
otp: optional(t.string),
/** Transaction hex to use instead of the original transaction */
tx: optional(t.string),
/** Private key in string form as an alternative to wallet passphrase */
xprv: optional(t.string),
/** If true, returns information about pending transactions without approving */
previewPendingTxs: optional(t.boolean),
/** Alternative ID for the pending approval */
pendingApprovalId: optional(t.string),
} as const;

/**
* Pending approval state enum
*/
export const PendingApprovalState = t.union([
t.literal('pending'),
t.literal('awaitingSignature'),
t.literal('pendingBitGoAdminApproval'),
t.literal('pendingIdVerification'),
t.literal('pendingCustodianApproval'),
t.literal('pendingFinalApproval'),
t.literal('approved'),
t.literal('processing'),
t.literal('rejected'),
]);

/**
* Pending approval type enum
*/
export const PendingApprovalType = t.union([
t.literal('userChangeRequest'),
t.literal('transactionRequest'),
t.literal('policyRuleRequest'),
t.literal('updateApprovalsRequiredRequest'),
t.literal('transactionRequestFull'),
]);

/**
* Build parameters for transaction request
* Allows any additional properties beyond the known 'type' field
*/
export const BuildParams = t.intersection([
t.partial({
/** Transaction type (e.g., fanout, consolidate) */
type: t.union([t.literal('fanout'), t.literal('consolidate')]),
}),
t.UnknownRecord,
]);

/**
* Transaction request info within pending approval
*/
export const TransactionRequestInfo = t.intersection([
t.type({
/** Coin-specific transaction parameters */
coinSpecific: t.UnknownRecord,
/** Transaction recipients */
recipients: t.unknown,
/** Build parameters for the transaction */
buildParams: BuildParams,
}),
t.partial({
/** Source wallet ID for the transaction */
sourceWallet: t.string,
}),
]);

/**
* Pending approval information structure
*/
export const PendingApprovalInfo = t.intersection([
t.type({
/** Type of pending approval */
type: PendingApprovalType,
}),
t.partial({
/** Transaction request details (if type is transactionRequest) */
transactionRequest: TransactionRequestInfo,
}),
]);

/**
* Pending approval data response
* Both approve and reject return the same structure
*/
export const PendingApprovalResponse = t.intersection([
t.type({
/** Pending approval unique identifier */
id: t.string,
/** Current state of the pending approval */
state: PendingApprovalState,
/** User ID of the pending approval creator */
creator: t.string,
/** Pending approval information and details */
info: PendingApprovalInfo,
}),
t.partial({
/** Wallet ID if this is a wallet-level approval */
wallet: t.string,
/** Enterprise ID if this is an enterprise-level approval */
enterprise: t.string,
/** Number of approvals required for this pending approval */
approvalsRequired: t.number,
/** Transaction request ID associated with this pending approval */
txRequestId: t.string,
}),
]);

/**
* Update Pending Approval
* Approve or reject a pending approval by its ID.
* Supports transaction approvals, policy rule changes, and user change requests.
*
* @operationId express.v2.pendingapprovals
* @tag express
*/
export const PutV2PendingApproval = httpRoute({
path: '/api/v2/{coin}/pendingapprovals/{id}',
method: 'PUT',
request: httpRequest({
params: PendingApprovalParams,
body: PendingApprovalRequestBody,
}),
response: {
/** Successfully updated pending approval */
200: PendingApprovalResponse,
/** Bad request or validation error */
400: BitgoExpressError,
},
});
Loading