Skip to content

Commit

Permalink
fix(rpc): favour zod types only
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranjamie committed Jan 24, 2025
1 parent 17ea4a6 commit 7617bf2
Show file tree
Hide file tree
Showing 11 changed files with 370 additions and 224 deletions.
2 changes: 1 addition & 1 deletion apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@
"readable-stream": "4.5.2",
"redux-persist": "6.0.0",
"uuid": "10.0.0",
"zod": "3.23.8"
"zod": "3.24.1"
},
"devDependencies": {
"@babel/core": "7.24.6",
Expand Down
2 changes: 1 addition & 1 deletion packages/models/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"dependencies": {
"@stacks/stacks-blockchain-api-types": "7.8.2",
"bignumber.js": "9.1.2",
"zod": "3.23.8"
"zod": "3.24.1"
},
"devDependencies": {
"@leather.io/prettier-config": "workspace:*",
Expand Down
4 changes: 4 additions & 0 deletions packages/models/src/types.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@ export type ReplaceTypes<T, Replacements extends { [K in keyof T]?: any }> = Omi
> & {
[K in keyof Replacements]: Replacements[K];
};

export type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
2 changes: 1 addition & 1 deletion packages/query/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"url-join": "5.0.0",
"uuid": "10.0.0",
"yup": "1.3.3",
"zod": "3.23.8"
"zod": "3.24.1"
},
"devDependencies": {
"@leather.io/prettier-config": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion packages/rpc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"@leather.io/models": "workspace:*",
"@stacks/network": "6.13.0",
"@stacks/transactions-v7": "npm:@stacks/[email protected]",
"zod": "3.23.8"
"zod": "3.24.1"
},
"devDependencies": {
"tsup": "8.1.0",
Expand Down
51 changes: 51 additions & 0 deletions packages/rpc/src/methods/get-addresses.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { addressResponseBodySchema, btcAddressBaseSchema, stxAddressSchema } from './get-addresses';

describe('getAddresses', () => {
const baseRespnseBodyBtc = {
symbol: 'BTC',
type: 'p2wpkh',
address: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa',
publicKey: '02d9b4b6e',
derivationPath: "m/44'/0'/0'/0/0",
};

const baseRespnseBodyStx = {
symbol: 'STX',
address: 'SP1P72Z370VRYK5V9V3YVQAS1Z4X6D6GKQJ8K2JGK',
publicKey: '02d9b4b6e',
};

describe('btcAddressBaseSchema', () => {
test('schema mathches test data', () => {
const result = btcAddressBaseSchema.safeParse(baseRespnseBodyBtc);
expect(result.success).toEqual(true);
});

test('schema allows additional values', () => {
const result = btcAddressBaseSchema.safeParse({
...baseRespnseBodyBtc,
additionalProperties: 'should not be allowed',
});
expect(result.success).toEqual(true);
});
});

describe('stxAddressSchema', () => {
test('schema allows additional values STX address values', () => {
const result = stxAddressSchema.safeParse({
...baseRespnseBodyStx,
additionalProperties: 'should not be allowed',
});
expect(result.success).toEqual(true);
});
});

describe('getAddressesResponseBody', () => {
test('schema matches test data', () => {
const result = addressResponseBodySchema.safeParse({
addresses: [baseRespnseBodyBtc, baseRespnseBodyStx],
});
expect(result.success).toEqual(true);
});
});
});
105 changes: 76 additions & 29 deletions packages/rpc/src/methods/get-addresses.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,89 @@
import { AllowAdditionalProperties } from '@leather.io/models';
import { z } from 'zod';

import { DefineRpcMethod, RpcRequest, RpcResponse } from '../rpc/schemas';
import {
DefineRpcMethod,
createRpcRequestSchema,
createRpcResponseSchema,
defaultErrorSchema,
} from '../rpc/schemas';

export type PaymentTypes = 'p2pkh' | 'p2sh' | 'p2wpkh-p2sh' | 'p2wpkh' | 'p2tr';
const rpcGetAddressesMethodName = 'getAddresses';

export interface BtcAddressBase extends AllowAdditionalProperties {
symbol: 'BTC';
type: PaymentTypes;
address: string;
publicKey: string;
derivationPath: string;
}
export const bitcoinPaymentTypesSchema = z.enum(['p2pkh', 'p2sh', 'p2wpkh-p2sh', 'p2wpkh', 'p2tr']);
export type BitcoinPaymentTypes = z.infer<typeof bitcoinPaymentTypesSchema>;

export interface NativeSegwitAddress extends BtcAddressBase {
type: 'p2wpkh';
}
/** @deprecated use `BitcoinPaymentTypes` */
export type PaymentTypes = BitcoinPaymentTypes;

export interface TaprootAddress extends BtcAddressBase {
type: 'p2tr';
tweakedPublicKey: string;
}
//
// Bitcoin
export const btcAddressBaseSchema = z.object({
symbol: z.literal('BTC'),
type: bitcoinPaymentTypesSchema,
address: z.string(),
publicKey: z.string(),
derivationPath: z.string(),
});

export type BtcAddress = NativeSegwitAddress | TaprootAddress;
export type BtcAddressBase = z.infer<typeof btcAddressBaseSchema>;

export interface StxAddress extends AllowAdditionalProperties {
symbol: 'STX';
address: string;
publicKey: string;
}
const nativeSegwitAddressSchema = btcAddressBaseSchema
.extend({
type: z.literal('p2wpkh'),
})
.passthrough();

export type Address = BtcAddress | StxAddress;
export type NativeSegwitAddress = z.infer<typeof nativeSegwitAddressSchema>;

export interface AddressResponseBody extends AllowAdditionalProperties {
addresses: Address[];
}
const taprootAddressSchema = btcAddressBaseSchema
.extend({
type: z.literal('p2tr'),
tweakedPublicKey: z.string(),
})
.passthrough();

export type GetAddressesRequest = RpcRequest<'getAddresses'>;
export type TaprootAddress = z.infer<typeof taprootAddressSchema>;

export type GetAddressesResponse = RpcResponse<AddressResponseBody>;
export const btcAddressSchema = z.discriminatedUnion('type', [
nativeSegwitAddressSchema,
taprootAddressSchema,
]);

export type BtcAddress = z.infer<typeof btcAddressSchema>;

//
// Stacks
export const stxAddressSchema = z
.object({
symbol: z.literal('STX'),
address: z.string(),
publicKey: z.string(),
})
.passthrough();

export type StxAddress = z.infer<typeof stxAddressSchema>;

export const addressSchema = z.union([btcAddressSchema, stxAddressSchema]);

export type Address = z.infer<typeof addressSchema>;

export const getAddressesRequestSchema = createRpcRequestSchema(rpcGetAddressesMethodName);

export type GetAddressesRequest = z.infer<typeof getAddressesRequestSchema>;

//
// Combined addresses response
export const addressResponseBodySchema = z
.object({ addresses: z.array(addressSchema) })
.passthrough();

export const getAddressesResponseSchema = createRpcResponseSchema(
addressResponseBodySchema,
defaultErrorSchema
);

export type AddressResponseBody = z.infer<typeof addressResponseBodySchema>;

export type GetAddressesResponse = z.infer<typeof getAddressesResponseSchema>;

export type DefineGetAddressesMethod = DefineRpcMethod<GetAddressesRequest, GetAddressesResponse>;
18 changes: 15 additions & 3 deletions packages/rpc/src/methods/stacks/stx-get-addresses.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { z } from 'zod';

import { DefineRpcMethod, RpcRequest, RpcResponse } from '../../rpc/schemas';
import {
DefineRpcMethod,
RpcResponse,
createRpcRequestSchema,
createRpcResponseSchema,
defaultErrorSchema,
} from '../../rpc/schemas';

export const stxGetAddressesMethodName = 'stx_getAddresses';

type StxGetAddressesRequestMethodName = typeof stxGetAddressesMethodName;
export type StxGetAddressesRequestMethodName = typeof stxGetAddressesMethodName;

// Request
export const stxGetAddressesRequestSchema = createRpcRequestSchema(stxGetAddressesMethodName);

export type StxGetAddressesRequest = RpcRequest<StxGetAddressesRequestMethodName>;
export type StxGetAddressesRequest = z.infer<typeof stxGetAddressesRequestSchema>;

// Result
export const stxAddressItemSchema = z.object({
Expand All @@ -19,6 +26,11 @@ export const stxAddressItemSchema = z.object({

export const stxGetAddressesResponseBodySchema = z.array(stxAddressItemSchema);

export const stxGetAddressesResponseSchema = createRpcResponseSchema(
stxGetAddressesResponseBodySchema,
defaultErrorSchema
);

export type StxGetAddressesResponse = RpcResponse<typeof stxGetAddressesResponseBodySchema>;

export type DefineStxGetAddressesMethod = DefineRpcMethod<
Expand Down
74 changes: 53 additions & 21 deletions packages/rpc/src/rpc/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,74 @@ export type RpcParameterByPosition = z.infer<typeof rpcParameterByPositionSchema
const rpcParameterByNameSchema = z.record(z.string(), z.unknown());
export type RpcParameterByName = z.infer<typeof rpcParameterByNameSchema>;

export const rpcParameterSchema = z.union([rpcParameterByPositionSchema, rpcParameterByNameSchema]);
export const rpcParameterSchema = z.union([
rpcParameterByPositionSchema,
rpcParameterByNameSchema,
z.undefined(),
]);
export type RpcParameter = z.infer<typeof rpcParameterSchema>;

export const rpcBasePropsSchema = z.object({
jsonrpc: z.literal('2.0'),
id: z.string(),
});

type BaseRpcRequestSchema = typeof rpcBasePropsSchema;
export type RpcBaseProps = z.infer<typeof rpcBasePropsSchema>;

//
// RPC Request
export function createRpcRequestSchema<TMethod extends z.ZodTypeAny, TParam extends z.ZodTypeAny>(
methodSchema: TMethod,
// {
// "jsonrpc": "2.0",
// "id": "123",
// "method": "signPsbt",
// "params": { "psbt": "dead…beef" },
// }
export function createRpcRequestSchema<TMethod extends string>(
method: TMethod
): BaseRpcRequestSchema & z.ZodObject<{ method: z.ZodLiteral<TMethod> }>;
export function createRpcRequestSchema<TMethod extends string, TParam extends z.ZodTypeAny>(
method: TMethod,
paramsSchema: TParam
): BaseRpcRequestSchema & z.ZodObject<{ method: z.ZodLiteral<TMethod>; params: TParam }>;
export function createRpcRequestSchema<TMethod extends string, TParam extends z.ZodTypeAny>(
method: TMethod,
paramsSchema?: TParam
) {
return rpcBasePropsSchema.extend({ method: methodSchema, params: paramsSchema });
// Unable to type this without the any, however the return type is corrects
if (!paramsSchema) return rpcBasePropsSchema.extend({ method: z.literal(method) }) as any;

return rpcBasePropsSchema.extend({
method: z.literal(method),
params: paramsSchema,
});
}

type RpcRequestSchema<TMethod, TParam extends RpcParameter = RpcParameter> = ReturnType<
typeof createRpcRequestSchema<z.ZodType<TMethod>, z.ZodType<TParam>>
type RpcRequestSchema<TMethod extends string, TParam extends RpcParameter> = ReturnType<
typeof createRpcRequestSchema<TMethod, z.ZodType<TParam>>
>;

export type RpcRequest<TMethod, TParam extends RpcParameter = RpcParameter> = z.infer<
export type RpcRequest<TMethod extends string, TParam extends RpcParameter = undefined> = z.infer<
RpcRequestSchema<TMethod, TParam>
>;

//
// RPC Error Body

export enum RpcErrorCode {
// Spec defined server errors
PARSE_ERROR = -32700,
INVALID_REQUEST = -32600,
METHOD_NOT_FOUND = -32601,
INVALID_PARAMS = -32602,
INTERNAL_ERROR = -32603,
SERVER_ERROR = -32000,
// Client defined errors
USER_REJECTION = 4001,
METHOD_NOT_SUPPORTED = 4002,
}
const rpcErrorCodeSchema = z.nativeEnum(RpcErrorCode);

export function createRpcErrorBodySchema<TErrorData extends z.ZodTypeAny>(
errorDataSchema: TErrorData
) {
Expand All @@ -63,6 +102,8 @@ export function createRpcErrorResponseSchema<
return rpcBasePropsSchema.extend({ error: errorSchema });
}

export const defaultErrorSchema = createRpcErrorBodySchema(z.any());

type RpcErrorResponseSchema<TError extends RpcErrorBody = RpcErrorBody> = ReturnType<
typeof createRpcErrorResponseSchema<z.ZodType<TError>>
>;
Expand All @@ -73,6 +114,11 @@ export type RpcErrorResponse<TError extends RpcErrorBody = RpcErrorBody> = z.inf

//
// RPC Success Response
// {
// "jsonrpc": "2.0",
// "id": "123",
// "result": { "signature": "dead…beef" }
// }
export function createRpcSuccessResponseSchema<TResult extends z.ZodType<object>>(
resultSchema: TResult
) {
Expand Down Expand Up @@ -105,20 +151,6 @@ export type RpcResponse<
TError extends RpcErrorBody = RpcErrorBody,
> = z.infer<RpcResponseSchema<TResult, TError>>;

export enum RpcErrorCode {
// Spec defined server errors
PARSE_ERROR = -32700,
INVALID_REQUEST = -32600,
METHOD_NOT_FOUND = -32601,
INVALID_PARAMS = -32602,
INTERNAL_ERROR = -32603,
SERVER_ERROR = -32000,
// Client defined errors
USER_REJECTION = 4001,
METHOD_NOT_SUPPORTED = 4002,
}
const rpcErrorCodeSchema = z.nativeEnum(RpcErrorCode);

export type ExtractSuccessResponse<T> = Extract<T, { result: any }>;

export type ExtractErrorResponse<T> = Extract<T, { error: any }>;
Expand Down
2 changes: 1 addition & 1 deletion packages/services/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"axios": "1.7.7",
"inversify": "6.0.2",
"p-queue": "8.0.1",
"zod": "3.23.8"
"zod": "3.24.1"
},
"devDependencies": {
"@leather.io/prettier-config": "workspace:*",
Expand Down
Loading

0 comments on commit 7617bf2

Please sign in to comment.