Skip to content

Commit 16a68a5

Browse files
committed
configurable custody service
1 parent bbf7778 commit 16a68a5

16 files changed

+721
-273
lines changed

packages/services/src/ctx/approver.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { PlainMessage } from '@bufbuild/protobuf';
2+
import { createContextKey } from '@connectrpc/connect';
3+
import {
4+
AuthorizeRequest,
5+
AuthorizeResponse,
6+
} from '@penumbra-zone/protobuf/penumbra/custody/v1/custody_pb';
7+
8+
export const authorizeCtx = createContextKey<
9+
(req: PlainMessage<AuthorizeRequest>) => Promise<PlainMessage<AuthorizeResponse>>
10+
>(undefined as never);

packages/services/src/ctx/spend-key.ts

Lines changed: 0 additions & 6 deletions
This file was deleted.

packages/services/src/custody-service/authorize.test.ts

Lines changed: 29 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,33 @@
1-
import { beforeEach, describe, expect, Mock, test, vi } from 'vitest';
1+
import { PlainMessage } from '@bufbuild/protobuf';
22
import { createContextValues, createHandlerContext, HandlerContext } from '@connectrpc/connect';
3-
import { approverCtx } from '../ctx/approver.js';
4-
import { servicesCtx } from '../ctx/prax.js';
5-
import { testFullViewingKey, testSpendKey } from '../test-utils.js';
6-
import { authorize } from './authorize.js';
7-
import { AuthorizeRequest } from '@penumbra-zone/protobuf/penumbra/custody/v1/custody_pb';
83
import { CustodyService } from '@penumbra-zone/protobuf';
4+
import { Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
95
import {
106
AuthorizationData,
117
TransactionPlan,
128
} from '@penumbra-zone/protobuf/penumbra/core/transaction/v1/transaction_pb';
9+
import {
10+
AuthorizeRequest,
11+
AuthorizeResponse,
12+
} from '@penumbra-zone/protobuf/penumbra/custody/v1/custody_pb';
1313
import type { ServicesInterface } from '@penumbra-zone/types/services';
14-
import { Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
15-
import { UserChoice } from '@penumbra-zone/types/user-choice';
14+
import { authorizePlan } from '@penumbra-zone/wasm/build';
15+
import { beforeEach, describe, expect, Mock, test, vi } from 'vitest';
16+
import { authorizeCtx } from '../ctx/authorize.js';
1617
import { fvkCtx } from '../ctx/full-viewing-key.js';
17-
import { skCtx } from '../ctx/spend-key.js';
18+
import { servicesCtx } from '../ctx/prax.js';
19+
import { testFullViewingKey, testSpendKey } from '../test-utils.js';
20+
import { authorize } from './authorize.js';
1821

1922
describe('Authorize request handler', () => {
2023
let req: AuthorizeRequest;
2124

22-
const mockApproverCtx = vi.fn(() => Promise.resolve<UserChoice>(UserChoice.Approved));
2325
const mockFullViewingKeyCtx = vi.fn(() => Promise.resolve(testFullViewingKey));
24-
const mockSpendKeyCtx = vi.fn(() => Promise.resolve(testSpendKey));
26+
const mockAuthorizeCtx = vi.fn(({ plan }: PlainMessage<AuthorizeRequest>) =>
27+
Promise.resolve(
28+
new AuthorizeResponse({ data: authorizePlan(testSpendKey, new TransactionPlan(plan)) }),
29+
),
30+
);
2531
const mockServicesCtx: Mock<[], Promise<ServicesInterface>> = vi.fn();
2632

2733
const handlerContextInit = {
@@ -33,10 +39,9 @@ describe('Authorize request handler', () => {
3339
};
3440

3541
const contextValues = createContextValues()
36-
.set(approverCtx, mockApproverCtx as unknown)
3742
.set(servicesCtx, mockServicesCtx)
3843
.set(fvkCtx, mockFullViewingKeyCtx)
39-
.set(skCtx, mockSpendKeyCtx);
44+
.set(authorizeCtx, mockAuthorizeCtx);
4045

4146
const mockCtx: HandlerContext = createHandlerContext({
4247
...handlerContextInit,
@@ -77,53 +82,23 @@ describe('Authorize request handler', () => {
7782
expect(data).toBeInstanceOf(AuthorizationData);
7883
});
7984

80-
test('should fail if user denies request', async () => {
81-
mockApproverCtx.mockResolvedValueOnce(UserChoice.Denied);
82-
await expect(authorize(req, mockCtx)).rejects.toThrow();
85+
test('should fail if context does not authorize', async () => {
86+
mockAuthorizeCtx.mockRejectedValueOnce(new Error('unique error'));
87+
await expect(authorize(req, mockCtx)).rejects.toThrow('unique error');
8388
});
8489

8590
test('should fail if plan is missing in request', async () => {
86-
await expect(authorize(new AuthorizeRequest(), mockCtx)).rejects.toThrow(
87-
'No plan included in request',
88-
);
91+
await expect(authorize(new AuthorizeRequest(), mockCtx)).rejects.toThrow('No actions planned');
8992
});
9093

91-
test('should fail if fullViewingKey context is not configured', async () => {
92-
const ctxWithoutFullViewingKey = createHandlerContext({
93-
...handlerContextInit,
94-
contextValues: createContextValues()
95-
.set(approverCtx, mockApproverCtx as unknown)
96-
.set(servicesCtx, mockServicesCtx)
97-
.set(skCtx, mockSpendKeyCtx),
98-
});
99-
await expect(authorize(req, ctxWithoutFullViewingKey)).rejects.toThrow('[failed_precondition]');
100-
});
101-
102-
test('should fail if spendKey context is not configured', async () => {
103-
const ctxWithoutSpendKey = createHandlerContext({
104-
...handlerContextInit,
105-
contextValues: createContextValues()
106-
.set(approverCtx, mockApproverCtx as unknown)
107-
.set(servicesCtx, mockServicesCtx)
108-
.set(fvkCtx, mockFullViewingKeyCtx),
109-
});
110-
await expect(authorize(req, ctxWithoutSpendKey)).rejects.toThrow('[failed_precondition]');
111-
});
112-
113-
test('should fail if approver context is not configured', async () => {
114-
const ctxWithoutApprover = createHandlerContext({
115-
...handlerContextInit,
116-
contextValues: createContextValues()
117-
.set(servicesCtx, mockServicesCtx)
118-
.set(fvkCtx, mockFullViewingKeyCtx)
119-
.set(skCtx, mockSpendKeyCtx),
120-
});
121-
await expect(authorize(req, ctxWithoutApprover)).rejects.toThrow('[failed_precondition]');
122-
});
94+
test('should fail if context is missing', async () => {
95+
const contextValues = createContextValues()
96+
.set(servicesCtx, mockServicesCtx)
97+
.set(fvkCtx, mockFullViewingKeyCtx);
12398

124-
test('should fail with reason if spendKey is not available', async () => {
125-
mockSpendKeyCtx.mockRejectedValueOnce(new Error('some reason'));
126-
await expect(authorize(req, mockCtx)).rejects.toThrow('some reason');
99+
await expect(
100+
authorize(req, createHandlerContext({ ...handlerContextInit, contextValues })),
101+
).rejects.toThrow('not a function');
127102
});
128103
});
129104

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,25 @@
1-
import type { Impl } from './index.js';
2-
import { approverCtx } from '../ctx/approver.js';
3-
import { authorizePlan } from '@penumbra-zone/wasm/build';
4-
import { Code, ConnectError } from '@connectrpc/connect';
5-
import { UserChoice } from '@penumbra-zone/types/user-choice';
1+
import { authorizeCtx } from '../ctx/authorize.js';
62
import { fvkCtx } from '../ctx/full-viewing-key.js';
7-
import { skCtx } from '../ctx/spend-key.js';
8-
import { assertValidAuthorizeRequest } from './validation/authorize.js';
9-
10-
export const authorize: Impl['authorize'] = async (req, ctx) => {
11-
if (!req.plan) {
12-
throw new ConnectError('No plan included in request', Code.InvalidArgument);
13-
}
3+
import type { Impl } from './index.js';
4+
import { assertValidActionPlans } from './util/validate-request.js';
5+
import { assertValidAuthorizationData } from './util/validate-response.js';
6+
import { toPlainMessage } from '@bufbuild/protobuf';
147

15-
const fullViewingKey = await ctx.values.get(fvkCtx)();
16-
assertValidAuthorizeRequest(req, fullViewingKey);
8+
/**
9+
* This is essentially a configurable stub. It performs some basic validation,
10+
* and then trusts the service context to provide an appropriate implementation.
11+
*/
12+
export const authorize: Impl['authorize'] = async ({ plan, preAuthorizations }, ctx) => {
13+
const fvk = await ctx.values.get(fvkCtx)();
1714

18-
const choice = await ctx.values.get(approverCtx)(req);
19-
if (choice !== UserChoice.Approved) {
20-
throw new ConnectError('Transaction was not approved', Code.PermissionDenied);
21-
}
15+
const actionCounts = assertValidActionPlans(fvk, plan && toPlainMessage(plan).actions);
2216

23-
const spendKey = await ctx.values.get(skCtx)();
17+
const { data: authorizationData } = await ctx.values.get(authorizeCtx)({
18+
plan,
19+
preAuthorizations,
20+
});
2421

25-
const data = authorizePlan(spendKey, req.plan);
26-
return { data };
22+
return {
23+
data: assertValidAuthorizationData(actionCounts, authorizationData),
24+
};
2725
};
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { ActionPlan } from '@penumbra-zone/protobuf/penumbra/core/transaction/v1/transaction_pb';
2+
3+
export const actionCountsInit: Record<NonNullable<ActionPlan['action']['case']>, number> = {
4+
actionDutchAuctionEnd: 0,
5+
actionDutchAuctionSchedule: 0,
6+
actionDutchAuctionWithdraw: 0,
7+
actionLiquidityTournamentVote: 0,
8+
communityPoolDeposit: 0,
9+
communityPoolOutput: 0,
10+
communityPoolSpend: 0,
11+
delegate: 0,
12+
delegatorVote: 0,
13+
ibcRelayAction: 0,
14+
ics20Withdrawal: 0,
15+
output: 0,
16+
positionClose: 0,
17+
positionOpen: 0,
18+
positionRewardClaim: 0,
19+
positionWithdraw: 0,
20+
proposalDepositClaim: 0,
21+
proposalSubmit: 0,
22+
proposalWithdraw: 0,
23+
spend: 0,
24+
swap: 0,
25+
swapClaim: 0,
26+
undelegate: 0,
27+
undelegateClaim: 0,
28+
validatorDefinition: 0,
29+
validatorVote: 0,
30+
} as const;

0 commit comments

Comments
 (0)