From 2724ac2e82ac8b8713aacf1939f03e98fd04b3f0 Mon Sep 17 00:00:00 2001 From: danielzhao122 Date: Tue, 30 Sep 2025 14:29:55 -0400 Subject: [PATCH 1/2] feat(express): migrate createAddress to typed routes TICKET: WP-5418 --- modules/express/src/clientRoutes.ts | 11 ++- modules/express/src/typedRoutes/api/index.ts | 4 + .../src/typedRoutes/api/v2/createAddress.ts | 75 +++++++++++++++++++ .../src/typedRoutes/schemas/address.ts | 10 +++ .../test/unit/clientRoutes/createAddress.ts | 19 +++-- .../express/test/unit/typedRoutes/decode.ts | 20 +++++ 6 files changed, 128 insertions(+), 11 deletions(-) create mode 100644 modules/express/src/typedRoutes/api/v2/createAddress.ts create mode 100644 modules/express/src/typedRoutes/schemas/address.ts diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index 90687ec2a1..a8c06e1c94 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -660,11 +660,11 @@ export async function handleV2GenerateWallet(req: express.Request) { * handle new address creation * @param req */ -export async function handleV2CreateAddress(req: express.Request) { +export async function handleV2CreateAddress(req: ExpressApiRouteRequest<'express.v2.wallet.createAddress', 'post'>) { const bitgo = req.bitgo; - const coin = bitgo.coin(req.params.coin); - const wallet = await coin.wallets().get({ id: req.params.id }); - return wallet.createAddress(req.body); + const coin = bitgo.coin(req.decoded.coin); + const wallet = await coin.wallets().get({ id: req.decoded.walletId }); + return wallet.createAddress(req.decoded); } /** @@ -1626,8 +1626,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void { promiseWrapper(handleKeychainChangePassword) ); - // create address - app.post('/api/v2/:coin/wallet/:id/address', parseBody, prepareBitGo(config), promiseWrapper(handleV2CreateAddress)); + router.post('express.v2.wallet.createAddress', [prepareBitGo(config), typedPromiseWrapper(handleV2CreateAddress)]); // share wallet app.post('/api/v2/:coin/wallet/:id/share', parseBody, prepareBitGo(config), promiseWrapper(handleV2ShareWallet)); diff --git a/modules/express/src/typedRoutes/api/index.ts b/modules/express/src/typedRoutes/api/index.ts index 47eecfed79..f35351e090 100644 --- a/modules/express/src/typedRoutes/api/index.ts +++ b/modules/express/src/typedRoutes/api/index.ts @@ -22,6 +22,7 @@ import { PostDeriveLocalKeyChain } from './v1/deriveLocalKeyChain'; import { PostCreateLocalKeyChain } from './v1/createLocalKeyChain'; import { PutConstructPendingApprovalTx } from './v1/constructPendingApprovalTx'; import { PutConsolidateUnspents } from './v1/consolidateUnspents'; +import { PostCreateAddress } from './v2/createAddress'; export const ExpressApi = apiSpec({ 'express.ping': { @@ -69,6 +70,9 @@ export const ExpressApi = apiSpec({ 'express.verifycoinaddress': { post: PostVerifyCoinAddress, }, + 'express.v2.wallet.createAddress': { + post: PostCreateAddress, + }, 'express.calculateminerfeeinfo': { post: PostCalculateMinerFeeInfo, }, diff --git a/modules/express/src/typedRoutes/api/v2/createAddress.ts b/modules/express/src/typedRoutes/api/v2/createAddress.ts new file mode 100644 index 0000000000..5e009ba7db --- /dev/null +++ b/modules/express/src/typedRoutes/api/v2/createAddress.ts @@ -0,0 +1,75 @@ +import * as t from 'io-ts'; +import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http'; +import { BitgoExpressError } from '../../schemas/error'; +import { EIP1559, ForwarderVersion, CreateAddressFormat } from '../../schemas/address'; + +/** + * Path parameters for creating a wallet address + */ +export const CreateAddressParams = { + /** Coin ticker / chain identifier */ + coin: t.string, + /** The ID of the wallet. */ + walletId: t.string, +} as const; + +/** + * Request body for creating a wallet address + */ +export const CreateAddressBody = { + /** Address type for chains that support multiple address types */ + type: optional(t.string), + /** Chain on which the new address should be created. Default: 1 */ + chain: optional(t.number), + /** + * (ETH only) Specify forwarder version to use in address creation. + * 0: legacy forwarder; + * 1: fee-improved forwarder; + * 2: NFT-supported forwarder (v2 wallets); + * 3: MPC wallets; + * 4: EVM variants; + * 5: new MPC wallets with wallet-version 6 + */ + forwarderVersion: optional(ForwarderVersion), + /** EVM keyring reference address (EVM only) */ + evmKeyRingReferenceAddress: optional(t.string), + /** Create an address for the given token (OFC only) (eg. ofcbtc) */ + onToken: optional(t.string), + /** A human-readable label for the address (Max length: 250) */ + label: optional(t.string), + /** Whether the deployment should use a low priority fee key (ETH only) Default: false */ + lowPriority: optional(t.boolean), + /** Explicit gas price to use when deploying the forwarder contract (ETH only) */ + gasPrice: optional(t.union([t.number, t.string])), + /** EIP1559 fee parameters (ETH forwarderVersion: 0 wallets only) */ + eip1559: optional(EIP1559), + /** Format to use for the new address (e.g., 'cashaddr' for BCH) */ + format: optional(CreateAddressFormat), + /** Number of new addresses to create (maximum 250) */ + count: optional(t.number), + /** Base address of the wallet (if applicable) */ + baseAddress: optional(t.string), + /** When false, throw error if address verification is skipped */ + allowSkipVerifyAddress: optional(t.boolean), +} as const; + +/** Response for creating a wallet address */ +export const CreateAddressResponse = { + 200: t.unknown, + 400: BitgoExpressError, +} as const; + +/** + * Create address for a wallet + * + * @operationId express.v2.wallet.createAddress + */ +export const PostCreateAddress = httpRoute({ + path: '/api/v2/{coin}/wallet/{walletId}/address', + method: 'POST', + request: httpRequest({ + params: CreateAddressParams, + body: CreateAddressBody, + }), + response: CreateAddressResponse, +}); diff --git a/modules/express/src/typedRoutes/schemas/address.ts b/modules/express/src/typedRoutes/schemas/address.ts new file mode 100644 index 0000000000..0e9a9b9b66 --- /dev/null +++ b/modules/express/src/typedRoutes/schemas/address.ts @@ -0,0 +1,10 @@ +import * as t from 'io-ts'; + +export const ForwarderVersion = t.union([t.literal(0), t.literal(1), t.literal(2), t.literal(3), t.literal(4)]); + +export const EIP1559 = t.type({ + maxFeePerGas: t.number, + maxPriorityFeePerGas: t.number, +}); + +export const CreateAddressFormat = t.union([t.literal('base58'), t.literal('cashaddr')]); diff --git a/modules/express/test/unit/clientRoutes/createAddress.ts b/modules/express/test/unit/clientRoutes/createAddress.ts index a932f69742..75941fdc6d 100644 --- a/modules/express/test/unit/clientRoutes/createAddress.ts +++ b/modules/express/test/unit/clientRoutes/createAddress.ts @@ -4,11 +4,12 @@ import 'should-http'; import 'should-sinon'; import '../../lib/asserts'; -import * as express from 'express'; - import { handleV2CreateAddress } from '../../../src/clientRoutes'; import { BitGo } from 'bitgo'; +import { ExpressApiRouteRequest } from '../../../src/typedRoutes/api'; +import { CreateAddressResponse } from '../../../src/typedRoutes/api/v2/createAddress'; +import { decodeOrElse } from '@bitgo/sdk-core'; describe('Create Address', () => { function createAddressMocks(res) { @@ -30,14 +31,22 @@ describe('Create Address', () => { bitgo: bitgoStub, params: { coin: 'tbtc', - id: '23423423423423', + walletId: '23423423423423', }, query: {}, body: { chain: 0, }, - } as unknown as express.Request; + decoded: { + coin: 'tbtc', + walletId: '23423423423423', + chain: 0, + }, + } as unknown as ExpressApiRouteRequest<'express.v2.wallet.createAddress', 'post'>; - await handleV2CreateAddress(req).should.be.resolvedWith(res); + const result = await handleV2CreateAddress(req).should.be.resolvedWith(res); + decodeOrElse('express.v2.wallet.createAddress', CreateAddressResponse[200], result, (errors) => { + throw new Error(`Response did not match expected codec: ${errors}`); + }); }); }); diff --git a/modules/express/test/unit/typedRoutes/decode.ts b/modules/express/test/unit/typedRoutes/decode.ts index 2356b36f46..ae7c15be90 100644 --- a/modules/express/test/unit/typedRoutes/decode.ts +++ b/modules/express/test/unit/typedRoutes/decode.ts @@ -13,6 +13,7 @@ import { LightningInitWalletParams, } from '../../../src/typedRoutes/api/v2/lightningInitWallet'; import { UnlockLightningWalletBody, UnlockLightningWalletParams } from '../../../src/typedRoutes/api/v2/unlockWallet'; +import { CreateAddressBody, CreateAddressParams } from '../../../src/typedRoutes/api/v2/createAddress'; export function assertDecode(codec: t.Type, input: unknown): T { const result = codec.decode(input); @@ -190,4 +191,23 @@ describe('io-ts decode tests', function () { // valid body assertDecode(t.type(UnlockLightningWalletBody), { passphrase: 'secret' }); }); + + it('express.v2.wallet.createAddress params', function () { + // missing walletId + assert.throws(() => assertDecode(t.type(CreateAddressParams), { coin: 'btc' })); + // coin must be string + assert.throws(() => + assertDecode(t.type(CreateAddressParams), { coin: 123, walletId: '59cd72485007a239fb00282ed480da1f' }) + ); + // walletId must be string + assert.throws(() => assertDecode(t.type(CreateAddressParams), { coin: 'btc', walletId: 123 })); + // valid params + assertDecode(t.type(CreateAddressParams), { coin: 'btc', walletId: '59cd72485007a239fb00282ed480da1f' }); + // invalid body + assert.throws(() => assertDecode(t.type(CreateAddressBody), { chain: '1' })); + assert.throws(() => assertDecode(t.type(CreateAddressBody), { format: 'invalid' })); + // valid body + assertDecode(t.type(CreateAddressBody), { eip1559: { maxFeePerGas: 1, maxPriorityFeePerGas: 1 } }); + assertDecode(t.type(CreateAddressBody), {}); + }); }); From 4753b915ce798368be764b6ca99a31da28a11d1a Mon Sep 17 00:00:00 2001 From: danielzhao122 Date: Thu, 2 Oct 2025 17:59:52 -0400 Subject: [PATCH 2/2] refactor: reverting walletId back to id TICKET: WP-5418 --- modules/express/src/clientRoutes.ts | 2 +- .../express/src/typedRoutes/api/v2/createAddress.ts | 3 ++- .../express/test/unit/clientRoutes/createAddress.ts | 4 ++-- modules/express/test/unit/typedRoutes/decode.ts | 10 +++++----- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index a8c06e1c94..7fe535c391 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -663,7 +663,7 @@ export async function handleV2GenerateWallet(req: express.Request) { export async function handleV2CreateAddress(req: ExpressApiRouteRequest<'express.v2.wallet.createAddress', 'post'>) { const bitgo = req.bitgo; const coin = bitgo.coin(req.decoded.coin); - const wallet = await coin.wallets().get({ id: req.decoded.walletId }); + const wallet = await coin.wallets().get({ id: req.decoded.id }); return wallet.createAddress(req.decoded); } diff --git a/modules/express/src/typedRoutes/api/v2/createAddress.ts b/modules/express/src/typedRoutes/api/v2/createAddress.ts index 5e009ba7db..9748d8fee3 100644 --- a/modules/express/src/typedRoutes/api/v2/createAddress.ts +++ b/modules/express/src/typedRoutes/api/v2/createAddress.ts @@ -10,7 +10,7 @@ export const CreateAddressParams = { /** Coin ticker / chain identifier */ coin: t.string, /** The ID of the wallet. */ - walletId: t.string, + id: t.string, } as const; /** @@ -62,6 +62,7 @@ export const CreateAddressResponse = { /** * Create address for a wallet * + * @tag express * @operationId express.v2.wallet.createAddress */ export const PostCreateAddress = httpRoute({ diff --git a/modules/express/test/unit/clientRoutes/createAddress.ts b/modules/express/test/unit/clientRoutes/createAddress.ts index 75941fdc6d..2f6c1c8e3e 100644 --- a/modules/express/test/unit/clientRoutes/createAddress.ts +++ b/modules/express/test/unit/clientRoutes/createAddress.ts @@ -31,7 +31,7 @@ describe('Create Address', () => { bitgo: bitgoStub, params: { coin: 'tbtc', - walletId: '23423423423423', + id: '23423423423423', }, query: {}, body: { @@ -39,7 +39,7 @@ describe('Create Address', () => { }, decoded: { coin: 'tbtc', - walletId: '23423423423423', + id: '23423423423423', chain: 0, }, } as unknown as ExpressApiRouteRequest<'express.v2.wallet.createAddress', 'post'>; diff --git a/modules/express/test/unit/typedRoutes/decode.ts b/modules/express/test/unit/typedRoutes/decode.ts index ae7c15be90..094d729f48 100644 --- a/modules/express/test/unit/typedRoutes/decode.ts +++ b/modules/express/test/unit/typedRoutes/decode.ts @@ -193,16 +193,16 @@ describe('io-ts decode tests', function () { }); it('express.v2.wallet.createAddress params', function () { - // missing walletId + // missing id assert.throws(() => assertDecode(t.type(CreateAddressParams), { coin: 'btc' })); // coin must be string assert.throws(() => - assertDecode(t.type(CreateAddressParams), { coin: 123, walletId: '59cd72485007a239fb00282ed480da1f' }) + assertDecode(t.type(CreateAddressParams), { coin: 123, id: '59cd72485007a239fb00282ed480da1f' }) ); - // walletId must be string - assert.throws(() => assertDecode(t.type(CreateAddressParams), { coin: 'btc', walletId: 123 })); + // id must be string + assert.throws(() => assertDecode(t.type(CreateAddressParams), { coin: 'btc', id: 123 })); // valid params - assertDecode(t.type(CreateAddressParams), { coin: 'btc', walletId: '59cd72485007a239fb00282ed480da1f' }); + assertDecode(t.type(CreateAddressParams), { coin: 'btc', id: '59cd72485007a239fb00282ed480da1f' }); // invalid body assert.throws(() => assertDecode(t.type(CreateAddressBody), { chain: '1' })); assert.throws(() => assertDecode(t.type(CreateAddressBody), { format: 'invalid' }));