Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(rpc): favour zod types only #828

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading