diff --git a/modules/express/src/clientRoutes.ts b/modules/express/src/clientRoutes.ts index 4453185ff6..3231e9cdde 100755 --- a/modules/express/src/clientRoutes.ts +++ b/modules/express/src/clientRoutes.ts @@ -651,11 +651,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.id }); + return wallet.createAddress(req.decoded); } /** @@ -1617,8 +1617,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 de9e2da3f9..befb278400 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'; import { PutFanoutUnspents } from './v1/fanoutUnspents'; import { PostOfcSignPayload } from './v2/ofcSignPayload'; @@ -71,6 +72,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..9748d8fee3 --- /dev/null +++ b/modules/express/src/typedRoutes/api/v2/createAddress.ts @@ -0,0 +1,76 @@ +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. */ + id: 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 + * + * @tag express + * @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..2f6c1c8e3e 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) { @@ -36,8 +37,16 @@ describe('Create Address', () => { body: { chain: 0, }, - } as unknown as express.Request; + decoded: { + coin: 'tbtc', + id: '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 37da2d05d9..7197997358 100644 --- a/modules/express/test/unit/typedRoutes/decode.ts +++ b/modules/express/test/unit/typedRoutes/decode.ts @@ -14,6 +14,7 @@ import { } from '../../../src/typedRoutes/api/v2/lightningInitWallet'; import { UnlockLightningWalletBody, UnlockLightningWalletParams } from '../../../src/typedRoutes/api/v2/unlockWallet'; import { OfcSignPayloadBody } from '../../../src/typedRoutes/api/v2/ofcSignPayload'; +import { CreateAddressBody, CreateAddressParams } from '../../../src/typedRoutes/api/v2/createAddress'; export function assertDecode(codec: t.Type, input: unknown): T { const result = codec.decode(input); @@ -223,4 +224,23 @@ describe('io-ts decode tests', function () { walletPassphrase: 'secret', }); }); + + it('express.v2.wallet.createAddress params', function () { + // missing id + assert.throws(() => assertDecode(t.type(CreateAddressParams), { coin: 'btc' })); + // coin must be string + assert.throws(() => + assertDecode(t.type(CreateAddressParams), { coin: 123, id: '59cd72485007a239fb00282ed480da1f' }) + ); + // id must be string + assert.throws(() => assertDecode(t.type(CreateAddressParams), { coin: 'btc', id: 123 })); + // valid params + 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' })); + // valid body + assertDecode(t.type(CreateAddressBody), { eip1559: { maxFeePerGas: 1, maxPriorityFeePerGas: 1 } }); + assertDecode(t.type(CreateAddressBody), {}); + }); });