Skip to content

Commit fd01770

Browse files
refactor: migrated generateWallet to typed routes
2 parents 098c23a + 95909b1 commit fd01770

File tree

9 files changed

+749
-44
lines changed

9 files changed

+749
-44
lines changed

modules/express/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"debug": "^3.1.0",
5151
"dotenv": "^16.0.0",
5252
"express": "4.21.2",
53+
"io-ts-types": "^0.5.16",
5354
"io-ts": "npm:@bitgo-forks/[email protected]",
5455
"io-ts-types": "^0.5.19",
5556
"lodash": "^4.17.20",

modules/express/src/clientRoutes.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -636,11 +636,11 @@ export async function handleV2OFCSignPayload(
636636
* handle new wallet creation
637637
* @param req
638638
*/
639-
export async function handleV2GenerateWallet(req: express.Request) {
639+
export async function handleV2GenerateWallet(req: ExpressApiRouteRequest<'express.wallet.generate', 'post'>) {
640640
const bitgo = req.bitgo;
641-
const coin = bitgo.coin(req.params.coin);
642-
const result = await coin.wallets().generateWallet(req.body);
643-
if (req.query.includeKeychains === 'false') {
641+
const coin = bitgo.coin(req.decoded.coin);
642+
const result = await coin.wallets().generateWallet(req.decoded);
643+
if ((req.decoded.includeKeychains as any) === false) {
644644
return result.wallet.toJSON();
645645
}
646646
return { ...result, wallet: result.wallet.toJSON() };
@@ -1608,7 +1608,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
16081608
router.post('express.keychain.local', [prepareBitGo(config), typedPromiseWrapper(handleV2CreateLocalKeyChain)]);
16091609

16101610
// generate wallet
1611-
app.post('/api/v2/:coin/wallet/generate', parseBody, prepareBitGo(config), promiseWrapper(handleV2GenerateWallet));
1611+
router.post('express.wallet.generate', [prepareBitGo(config), typedPromiseWrapper(handleV2GenerateWallet)]);
16121612

16131613
router.put('express.wallet.update', [prepareBitGo(config), typedPromiseWrapper(handleWalletUpdate)]);
16141614

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { PostCreateAddress } from './v2/createAddress';
2727
import { PutFanoutUnspents } from './v1/fanoutUnspents';
2828
import { PostOfcSignPayload } from './v2/ofcSignPayload';
2929
import { PostWalletRecoverToken } from './v2/walletRecoverToken';
30+
import { PostGenerateWallet } from './v2/generateWallet';
3031
import { PostCoinSignTx } from './v2/coinSignTx';
3132
import { PostWalletSignTx } from './v2/walletSignTx';
3233
import { PostWalletTxSignTSS } from './v2/walletTxSignTSS';
@@ -207,6 +208,9 @@ export const ExpressOfcSignPayloadApiSpec = apiSpec({
207208
'express.wallet.update': {
208209
put: PutExpressWalletUpdate,
209210
},
211+
'express.wallet.generate': {
212+
post: PostGenerateWallet,
213+
},
210214
});
211215

212216
export type ExpressApi = typeof ExpressPingApiSpec &
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import * as t from 'io-ts';
2+
import { BooleanFromString } from 'io-ts-types';
3+
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
4+
import { BitgoExpressError } from '../../schemas/error';
5+
import { UserKeychainCodec, BackupKeychainCodec, BitgoKeychainCodec } from '../../schemas/keychain';
6+
import { multisigType, walletType } from '../../schemas/wallet';
7+
8+
/**
9+
* Request body for wallet generation.
10+
*/
11+
export const GenerateWalletBody = {
12+
/** Wallet label */
13+
label: t.string,
14+
/** Enterprise id. Required for Ethereum wallets since they can only be created as part of an enterprise. Optional for other coins. */
15+
enterprise: optional(t.string),
16+
/** If absent, BitGo uses the default wallet type for the asset */
17+
multisigType: optional(multisigType),
18+
/** The type of wallet, defined by key management and signing protocols. 'hot' and 'cold' are both self-managed wallets. If absent, defaults to 'hot' */
19+
type: optional(walletType),
20+
/** Passphrase to be used to encrypt the user key on the wallet */
21+
passphrase: optional(t.string),
22+
/** User provided public key */
23+
userKey: optional(t.string),
24+
/** Backup extended public key */
25+
backupXpub: optional(t.string),
26+
/** Optional key recovery service to provide and store the backup key */
27+
backupXpubProvider: optional(t.literal('dai')),
28+
/** Flag for disabling wallet transaction notifications */
29+
disableTransactionNotifications: optional(t.boolean),
30+
/** The passphrase used for decrypting the encrypted wallet passphrase during wallet recovery */
31+
passcodeEncryptionCode: optional(t.string),
32+
/** Seed that derives an extended user key or common keychain for a cold wallet */
33+
coldDerivationSeed: optional(t.string),
34+
/** Gas price to use when deploying an Ethereum wallet */
35+
gasPrice: optional(t.number),
36+
/** Flag for preventing KRS from sending email after creating backup key */
37+
disableKRSEmail: optional(t.boolean),
38+
/** (ETH only) Specify the wallet creation contract version used when creating a wallet contract */
39+
walletVersion: optional(t.number),
40+
/** True, if the wallet type is a distributed-custodial. If passed, you must also pass the 'enterprise' parameter */
41+
isDistributedCustody: optional(t.boolean),
42+
/** BitGo key ID for self-managed cold MPC wallets */
43+
bitgoKeyId: optional(t.string),
44+
/** Common keychain for self-managed cold MPC wallets */
45+
commonKeychain: optional(t.string),
46+
} as const;
47+
48+
export const GenerateWalletResponse200 = t.union([
49+
t.UnknownRecord,
50+
t.type({
51+
wallet: t.UnknownRecord,
52+
encryptedWalletPassphrase: optional(t.string),
53+
userKeychain: optional(UserKeychainCodec),
54+
backupKeychain: optional(BackupKeychainCodec),
55+
bitgoKeychain: optional(BitgoKeychainCodec),
56+
warning: optional(t.string),
57+
}),
58+
]);
59+
60+
/**
61+
* Response body for wallet generation.
62+
*/
63+
export const GenerateWalletResponse = {
64+
/** The newly created wallet */
65+
200: GenerateWalletResponse200,
66+
/** Bad request */
67+
400: BitgoExpressError,
68+
} as const;
69+
70+
/**
71+
* Path parameters for wallet generation.
72+
*/
73+
export const GenerateWalletV2Params = {
74+
/** Coin ticker / chain identifier */
75+
coin: t.string,
76+
};
77+
78+
/**
79+
* Query parameters for wallet generation.
80+
* @property includeKeychains - Include user, backup and bitgo keychains along with generated wallet
81+
*/
82+
export const GenerateWalletV2Query = {
83+
/** Include user, backup and bitgo keychains along with generated wallet */
84+
includeKeychains: optional(BooleanFromString),
85+
};
86+
87+
/**
88+
* Generate Wallet
89+
*
90+
* This API call creates a new wallet. Under the hood, the SDK (or BitGo Express) does the following:
91+
*
92+
* 1. Creates the user keychain locally on the machine, and encrypts it with the provided passphrase (skipped if userKey is provided).
93+
* 2. Creates the backup keychain locally on the machine.
94+
* 3. Uploads the encrypted user keychain and public backup keychain.
95+
* 4. Creates the BitGo key (and the backup key if backupXpubProvider is set) on the service.
96+
* 5. Creates the wallet on BitGo with the 3 public keys above.
97+
*
98+
* ⓘ Ethereum wallets can only be created under an enterprise. Pass in the id of the enterprise to associate the wallet with. Your enterprise id can be seen by clicking on the "Manage Organization" link on the enterprise dropdown. Each enterprise has a fee address which will be used to pay for transaction fees on all Ethereum wallets in that enterprise. The fee address is displayed in the dashboard of the website, please fund it before creating a wallet.
99+
*
100+
* ⓘ You cannot generate a wallet by passing in a subtoken as the coin. Subtokens share wallets with their parent coin and it is not possible to create a wallet specific to one token.
101+
*
102+
* ⓘ This endpoint should be called through BitGo Express if used without the SDK, such as when using cURL.
103+
*
104+
* @operationId express.wallet.generate
105+
*/
106+
export const PostGenerateWallet = httpRoute({
107+
path: '/api/v2/:coin/wallet/generate',
108+
method: 'POST',
109+
request: httpRequest({
110+
params: GenerateWalletV2Params,
111+
query: GenerateWalletV2Query,
112+
body: GenerateWalletBody,
113+
}),
114+
response: GenerateWalletResponse,
115+
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import * as t from 'io-ts';
2+
3+
// Base keychain fields
4+
const BaseKeychainCodec = t.type({
5+
id: t.string,
6+
pub: t.string,
7+
source: t.string,
8+
});
9+
10+
// User keychain: can have encryptedPrv and prv
11+
export const UserKeychainCodec = t.intersection([
12+
BaseKeychainCodec,
13+
t.partial({
14+
ethAddress: t.string,
15+
coinSpecific: t.UnknownRecord,
16+
encryptedPrv: t.string,
17+
prv: t.string,
18+
}),
19+
]);
20+
21+
// Backup keychain: can have prv
22+
export const BackupKeychainCodec = t.intersection([
23+
BaseKeychainCodec,
24+
t.partial({
25+
ethAddress: t.string,
26+
coinSpecific: t.UnknownRecord,
27+
prv: t.string,
28+
}),
29+
]);
30+
31+
// BitGo keychain: must have isBitGo
32+
export const BitgoKeychainCodec = t.intersection([
33+
BaseKeychainCodec,
34+
t.type({
35+
isBitGo: t.boolean,
36+
}),
37+
t.partial({
38+
ethAddress: t.string,
39+
coinSpecific: t.UnknownRecord,
40+
}),
41+
]);

modules/express/src/typedRoutes/schemas/wallet.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,10 @@ export const CustomChangeKeySignatures = t.partial({
138138
bitgo: t.string,
139139
});
140140

141+
export const multisigType = t.union([t.literal('onchain'), t.literal('tss')]);
142+
143+
export const walletType = t.union([t.literal('cold'), t.literal('custodial'), t.literal('hot'), t.literal('trading')]);
144+
141145
/**
142146
* Wallet response data
143147
* Comprehensive wallet information returned from wallet operations
@@ -175,11 +179,11 @@ export const WalletResponse = t.partial({
175179
/** Enterprise ID this wallet belongs to */
176180
enterprise: t.string,
177181
/** Wallet type (e.g., 'hot', 'cold', 'custodial') */
178-
type: t.string,
182+
type: walletType,
179183
/** Wallet subtype (e.g., 'lightningSelfCustody') */
180184
subType: t.string,
181185
/** Multisig type ('onchain' or 'tss') */
182-
multisigType: t.union([t.literal('onchain'), t.literal('tss')]),
186+
multisigType: multisigType,
183187
/** Multisig type version (e.g., 'MPCv2') */
184188
multisigTypeVersion: t.string,
185189
/** Coin-specific wallet data */

0 commit comments

Comments
 (0)