Skip to content

Commit

Permalink
refactor(rpc): add stx_signTransaction, test schemas, closes leather-…
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranjamie committed Nov 15, 2024
1 parent ac5231b commit 314a880
Show file tree
Hide file tree
Showing 15 changed files with 241 additions and 54 deletions.
3 changes: 2 additions & 1 deletion packages/rpc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
"zod": "3.23.8"
},
"devDependencies": {
"tsup": "8.1.0"
"tsup": "8.1.0",
"vitest": "2.0.5"
},
"files": [
"dist"
Expand Down
36 changes: 28 additions & 8 deletions packages/rpc/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,37 @@ import { DefineSendTransferMethod } from './methods/send-transfer';
import { DefineSignMessageMethod } from './methods/sign-message';
import { DefineSignPsbtMethod } from './methods/sign-psbt';
import { DefineStxSignMessageMethod } from './methods/stx-sign-message';
import { ExtractSuccessResponse } from './rpc';
import { DefineStxSignTransactionMethod } from './methods/stx-sign-transaction';
import { ExtractErrorResponse, ExtractSuccessResponse } from './rpc/schemas';

export * from './rpc';
export * from './rpc/schemas';
export * from './methods/get-info';
export * from './methods/sign-psbt';
export * from './methods/get-addresses';
export * from './methods/send-transfer';
export * from './methods/sign-message';
export * from './methods/stx-sign-message';

export type MethodMap = DefineGetInfoMethod &
export type LeatherRpcMethodMap = DefineGetInfoMethod &
DefineGetAddressesMethod &
DefineSignPsbtMethod &
DefineSignMessageMethod &
DefineSendTransferMethod &
DefineStxSignMessageMethod;
DefineStxSignMessageMethod &
DefineStxSignTransactionMethod;

export type RpcRequests = ValueOf<MethodMap>['request'];
export type RpcRequests = ValueOf<LeatherRpcMethodMap>['request'];

export type RpcResponses = ValueOf<MethodMap>['response'];
export type RpcResponses = ValueOf<LeatherRpcMethodMap>['response'];

export type MethodNames = keyof MethodMap;
export type MethodNames = keyof LeatherRpcMethodMap;

export interface RequestFn {
<T extends MethodNames>(
arg: T,
params?: object | string[]
// `Promise` throws if unsucessful, so here we extract the successful response
): Promise<ExtractSuccessResponse<MethodMap[T]['response']>>;
): Promise<ExtractSuccessResponse<LeatherRpcMethodMap[T]['response']>>;
}

export interface ListenFn {
Expand Down Expand Up @@ -64,3 +68,19 @@ export interface LeatherProvider {
*/
listen: ListenFn;
}

export function createRpcSuccessResponse<T extends MethodNames>(
_method: T,
response: Omit<ExtractSuccessResponse<LeatherRpcMethodMap[T]['response']>, 'jsonrpc'>
) {
return { jsonrpc: '2.0', ...response } as ExtractSuccessResponse<
LeatherRpcMethodMap[T]['response']
>;
}

export function createRpcErrorResponse<T extends MethodNames>(
_method: T,
error: Omit<ExtractErrorResponse<LeatherRpcMethodMap[T]['response']>['error'], 'jsonrpc'>
) {
return { jsonrpc: '2.0', error } as ExtractErrorResponse<LeatherRpcMethodMap[T]['response']>;
}
2 changes: 1 addition & 1 deletion packages/rpc/src/methods/get-addresses.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AllowAdditionalProperties } from '@leather.io/models';

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

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

Expand Down
2 changes: 1 addition & 1 deletion packages/rpc/src/methods/get-info.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DefineRpcMethod, RpcRequest, RpcResponse } from '../rpc';
import { DefineRpcMethod, RpcRequest, RpcResponse } from '../rpc/schemas';

interface GetInfoResponseBody {
version: string;
Expand Down
2 changes: 1 addition & 1 deletion packages/rpc/src/methods/send-transfer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DefineRpcMethod, RpcRequest, RpcResponse } from '../rpc';
import { DefineRpcMethod, RpcRequest, RpcResponse } from '../rpc/schemas';

export interface SendTransferRequestParams {
account?: number;
Expand Down
2 changes: 1 addition & 1 deletion packages/rpc/src/methods/sign-message.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AllowAdditionalProperties } from '@leather.io/models';

import { DefineRpcMethod, RpcRequest, RpcResponse } from '../rpc';
import { DefineRpcMethod, RpcRequest, RpcResponse } from '../rpc/schemas';
import { PaymentTypes } from './get-addresses';

// Implements BIP-322
Expand Down
2 changes: 1 addition & 1 deletion packages/rpc/src/methods/sign-psbt.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DefaultNetworkConfigurations } from '@leather.io/models';

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

/**
* DEFAULT -- all inputs, all outputs
Expand Down
6 changes: 4 additions & 2 deletions packages/rpc/src/methods/stx-sign-message.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { DefineRpcMethod, RpcRequest, RpcResponse } from '../rpc';
import { DefineRpcMethod, RpcRequest, RpcResponse } from '../rpc/schemas';

export type StxSignMessageTypes = 'utf8' | 'structured';
export const stxMessageSigningTypes = ['utf8', 'structured'] as const;

export type StxSignMessageTypes = (typeof stxMessageSigningTypes)[number];

export interface StxSignMessageRequestParamsBase {
type: StxSignMessageTypes;
Expand Down
29 changes: 29 additions & 0 deletions packages/rpc/src/methods/stx-sign-transaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { z } from 'zod';

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

export const stxSignTransactionMethodName = 'stx_signTransaction' as const;

const stxSignTransactionRequestParamsSchema = z.object({
txHex: z.string(),
stxAddress: z.string().optional(),
attachment: z.string().optional(),
});

export type StxSignTransactionRequestParams = z.infer<typeof stxSignTransactionRequestParamsSchema>;

const stxSignTransactionResponseSchema = z.object({ txHex: z.string() });

export type StxSignTransactionResponseBody = z.infer<typeof stxSignTransactionResponseSchema>;

export type StxSignTransactionRequest = RpcRequest<
typeof stxSignTransactionMethodName,
StxSignTransactionRequestParams
>;

export type StxSignTransactionResponse = RpcResponse<StxSignTransactionResponseBody>;

export type DefineStxSignTransactionMethod = DefineRpcMethod<
StxSignTransactionRequest,
StxSignTransactionResponse
>;
Empty file added packages/rpc/src/rpc/helpers.ts
Empty file.
107 changes: 107 additions & 0 deletions packages/rpc/src/rpc/schemas.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { z } from 'zod';

import {
createRpcErrorResponseSchema,
createRpcSuccessResponseSchema,
rpcBasePropsSchema,
} from './schemas';

const baseRequestData = { jsonrpc: '2.0', id: '1' };

describe('RPC method schemas', () => {
test('the base RPC schema', () => {
expect(rpcBasePropsSchema.safeParse(baseRequestData).success).toEqual(true);
});

describe(createRpcSuccessResponseSchema.name, () => {
test('validates a successful response with basic result object', () => {
const schema = createRpcSuccessResponseSchema(z.object({ value: z.string() }));
const validResponse = { ...baseRequestData, result: { value: 'test' } };

expect(schema.safeParse(validResponse).success).toBe(true);
});

test('fails validation when missing required RPC base properties', () => {
const schema = createRpcSuccessResponseSchema(z.object({ value: z.string() }));
const invalidResponse = { result: { value: 'test' } };

expect(schema.safeParse(invalidResponse).success).toBe(false);
});

test('fails validation when result does not match schema', () => {
const schema = createRpcSuccessResponseSchema(z.object({ value: z.number() }));
const invalidResponse = { ...baseRequestData, result: { value: 'not a number' } };

expect(schema.safeParse(invalidResponse).success).toBe(false);
});
});

describe(createRpcErrorResponseSchema.name, () => {
const rpcErrorSchema = z.object({
code: z.number(),
message: z.string(),
data: z.object({}).optional(),
});

test('validates error response with basic error object', () => {
const schema = createRpcErrorResponseSchema(rpcErrorSchema);
const validResponse = {
...baseRequestData,
error: {
code: -32600,
message: 'Invalid Request',
},
};

expect(schema.safeParse(validResponse).success).toBe(true);
});

test('validates error response with optional error data', () => {
const schema = createRpcErrorResponseSchema(rpcErrorSchema);
const validResponse = {
...baseRequestData,
error: {
code: -32603,
message: 'Internal error',
data: { details: 'Database connection failed' },
},
};

expect(schema.safeParse(validResponse).success).toBe(true);
});

test('fails validation when missing required RPC properties', () => {
const schema = createRpcErrorResponseSchema(rpcErrorSchema);
const invalidResponse = {
error: {
code: -32600,
message: 'Invalid Request',
},
};
expect(schema.safeParse(invalidResponse).success).toBe(false);
});

test('fails validation with invalid error code', () => {
const schema = createRpcErrorResponseSchema(rpcErrorSchema);
const invalidResponse = {
...baseRequestData,
error: {
code: 'not-a-number',
message: 'Invalid Request',
},
};

expect(schema.safeParse(invalidResponse).success).toBe(false);
});

test('fails validation when missing error message', () => {
const schema = createRpcErrorResponseSchema(rpcErrorSchema);
const invalidResponse = {
...baseRequestData,
error: { code: -32600 },
};

expect(schema.safeParse(invalidResponse).success).toBe(false);
});
});
});
Loading

0 comments on commit 314a880

Please sign in to comment.