Skip to content

Commit 2724ac2

Browse files
committed
feat(express): migrate createAddress to typed routes
TICKET: WP-5418
1 parent 3a9322c commit 2724ac2

File tree

6 files changed

+128
-11
lines changed

6 files changed

+128
-11
lines changed

modules/express/src/clientRoutes.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -660,11 +660,11 @@ export async function handleV2GenerateWallet(req: express.Request) {
660660
* handle new address creation
661661
* @param req
662662
*/
663-
export async function handleV2CreateAddress(req: express.Request) {
663+
export async function handleV2CreateAddress(req: ExpressApiRouteRequest<'express.v2.wallet.createAddress', 'post'>) {
664664
const bitgo = req.bitgo;
665-
const coin = bitgo.coin(req.params.coin);
666-
const wallet = await coin.wallets().get({ id: req.params.id });
667-
return wallet.createAddress(req.body);
665+
const coin = bitgo.coin(req.decoded.coin);
666+
const wallet = await coin.wallets().get({ id: req.decoded.walletId });
667+
return wallet.createAddress(req.decoded);
668668
}
669669

670670
/**
@@ -1626,8 +1626,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
16261626
promiseWrapper(handleKeychainChangePassword)
16271627
);
16281628

1629-
// create address
1630-
app.post('/api/v2/:coin/wallet/:id/address', parseBody, prepareBitGo(config), promiseWrapper(handleV2CreateAddress));
1629+
router.post('express.v2.wallet.createAddress', [prepareBitGo(config), typedPromiseWrapper(handleV2CreateAddress)]);
16311630

16321631
// share wallet
16331632
app.post('/api/v2/:coin/wallet/:id/share', parseBody, prepareBitGo(config), promiseWrapper(handleV2ShareWallet));

modules/express/src/typedRoutes/api/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { PostDeriveLocalKeyChain } from './v1/deriveLocalKeyChain';
2222
import { PostCreateLocalKeyChain } from './v1/createLocalKeyChain';
2323
import { PutConstructPendingApprovalTx } from './v1/constructPendingApprovalTx';
2424
import { PutConsolidateUnspents } from './v1/consolidateUnspents';
25+
import { PostCreateAddress } from './v2/createAddress';
2526

2627
export const ExpressApi = apiSpec({
2728
'express.ping': {
@@ -69,6 +70,9 @@ export const ExpressApi = apiSpec({
6970
'express.verifycoinaddress': {
7071
post: PostVerifyCoinAddress,
7172
},
73+
'express.v2.wallet.createAddress': {
74+
post: PostCreateAddress,
75+
},
7276
'express.calculateminerfeeinfo': {
7377
post: PostCalculateMinerFeeInfo,
7478
},
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import * as t from 'io-ts';
2+
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
3+
import { BitgoExpressError } from '../../schemas/error';
4+
import { EIP1559, ForwarderVersion, CreateAddressFormat } from '../../schemas/address';
5+
6+
/**
7+
* Path parameters for creating a wallet address
8+
*/
9+
export const CreateAddressParams = {
10+
/** Coin ticker / chain identifier */
11+
coin: t.string,
12+
/** The ID of the wallet. */
13+
walletId: t.string,
14+
} as const;
15+
16+
/**
17+
* Request body for creating a wallet address
18+
*/
19+
export const CreateAddressBody = {
20+
/** Address type for chains that support multiple address types */
21+
type: optional(t.string),
22+
/** Chain on which the new address should be created. Default: 1 */
23+
chain: optional(t.number),
24+
/**
25+
* (ETH only) Specify forwarder version to use in address creation.
26+
* 0: legacy forwarder;
27+
* 1: fee-improved forwarder;
28+
* 2: NFT-supported forwarder (v2 wallets);
29+
* 3: MPC wallets;
30+
* 4: EVM variants;
31+
* 5: new MPC wallets with wallet-version 6
32+
*/
33+
forwarderVersion: optional(ForwarderVersion),
34+
/** EVM keyring reference address (EVM only) */
35+
evmKeyRingReferenceAddress: optional(t.string),
36+
/** Create an address for the given token (OFC only) (eg. ofcbtc) */
37+
onToken: optional(t.string),
38+
/** A human-readable label for the address (Max length: 250) */
39+
label: optional(t.string),
40+
/** Whether the deployment should use a low priority fee key (ETH only) Default: false */
41+
lowPriority: optional(t.boolean),
42+
/** Explicit gas price to use when deploying the forwarder contract (ETH only) */
43+
gasPrice: optional(t.union([t.number, t.string])),
44+
/** EIP1559 fee parameters (ETH forwarderVersion: 0 wallets only) */
45+
eip1559: optional(EIP1559),
46+
/** Format to use for the new address (e.g., 'cashaddr' for BCH) */
47+
format: optional(CreateAddressFormat),
48+
/** Number of new addresses to create (maximum 250) */
49+
count: optional(t.number),
50+
/** Base address of the wallet (if applicable) */
51+
baseAddress: optional(t.string),
52+
/** When false, throw error if address verification is skipped */
53+
allowSkipVerifyAddress: optional(t.boolean),
54+
} as const;
55+
56+
/** Response for creating a wallet address */
57+
export const CreateAddressResponse = {
58+
200: t.unknown,
59+
400: BitgoExpressError,
60+
} as const;
61+
62+
/**
63+
* Create address for a wallet
64+
*
65+
* @operationId express.v2.wallet.createAddress
66+
*/
67+
export const PostCreateAddress = httpRoute({
68+
path: '/api/v2/{coin}/wallet/{walletId}/address',
69+
method: 'POST',
70+
request: httpRequest({
71+
params: CreateAddressParams,
72+
body: CreateAddressBody,
73+
}),
74+
response: CreateAddressResponse,
75+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as t from 'io-ts';
2+
3+
export const ForwarderVersion = t.union([t.literal(0), t.literal(1), t.literal(2), t.literal(3), t.literal(4)]);
4+
5+
export const EIP1559 = t.type({
6+
maxFeePerGas: t.number,
7+
maxPriorityFeePerGas: t.number,
8+
});
9+
10+
export const CreateAddressFormat = t.union([t.literal('base58'), t.literal('cashaddr')]);

modules/express/test/unit/clientRoutes/createAddress.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import 'should-http';
44
import 'should-sinon';
55
import '../../lib/asserts';
66

7-
import * as express from 'express';
8-
97
import { handleV2CreateAddress } from '../../../src/clientRoutes';
108

119
import { BitGo } from 'bitgo';
10+
import { ExpressApiRouteRequest } from '../../../src/typedRoutes/api';
11+
import { CreateAddressResponse } from '../../../src/typedRoutes/api/v2/createAddress';
12+
import { decodeOrElse } from '@bitgo/sdk-core';
1213

1314
describe('Create Address', () => {
1415
function createAddressMocks(res) {
@@ -30,14 +31,22 @@ describe('Create Address', () => {
3031
bitgo: bitgoStub,
3132
params: {
3233
coin: 'tbtc',
33-
id: '23423423423423',
34+
walletId: '23423423423423',
3435
},
3536
query: {},
3637
body: {
3738
chain: 0,
3839
},
39-
} as unknown as express.Request;
40+
decoded: {
41+
coin: 'tbtc',
42+
walletId: '23423423423423',
43+
chain: 0,
44+
},
45+
} as unknown as ExpressApiRouteRequest<'express.v2.wallet.createAddress', 'post'>;
4046

41-
await handleV2CreateAddress(req).should.be.resolvedWith(res);
47+
const result = await handleV2CreateAddress(req).should.be.resolvedWith(res);
48+
decodeOrElse('express.v2.wallet.createAddress', CreateAddressResponse[200], result, (errors) => {
49+
throw new Error(`Response did not match expected codec: ${errors}`);
50+
});
4251
});
4352
});

modules/express/test/unit/typedRoutes/decode.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
LightningInitWalletParams,
1414
} from '../../../src/typedRoutes/api/v2/lightningInitWallet';
1515
import { UnlockLightningWalletBody, UnlockLightningWalletParams } from '../../../src/typedRoutes/api/v2/unlockWallet';
16+
import { CreateAddressBody, CreateAddressParams } from '../../../src/typedRoutes/api/v2/createAddress';
1617

1718
export function assertDecode<T>(codec: t.Type<T, unknown>, input: unknown): T {
1819
const result = codec.decode(input);
@@ -190,4 +191,23 @@ describe('io-ts decode tests', function () {
190191
// valid body
191192
assertDecode(t.type(UnlockLightningWalletBody), { passphrase: 'secret' });
192193
});
194+
195+
it('express.v2.wallet.createAddress params', function () {
196+
// missing walletId
197+
assert.throws(() => assertDecode(t.type(CreateAddressParams), { coin: 'btc' }));
198+
// coin must be string
199+
assert.throws(() =>
200+
assertDecode(t.type(CreateAddressParams), { coin: 123, walletId: '59cd72485007a239fb00282ed480da1f' })
201+
);
202+
// walletId must be string
203+
assert.throws(() => assertDecode(t.type(CreateAddressParams), { coin: 'btc', walletId: 123 }));
204+
// valid params
205+
assertDecode(t.type(CreateAddressParams), { coin: 'btc', walletId: '59cd72485007a239fb00282ed480da1f' });
206+
// invalid body
207+
assert.throws(() => assertDecode(t.type(CreateAddressBody), { chain: '1' }));
208+
assert.throws(() => assertDecode(t.type(CreateAddressBody), { format: 'invalid' }));
209+
// valid body
210+
assertDecode(t.type(CreateAddressBody), { eip1559: { maxFeePerGas: 1, maxPriorityFeePerGas: 1 } });
211+
assertDecode(t.type(CreateAddressBody), {});
212+
});
193213
});

0 commit comments

Comments
 (0)