Skip to content

Commit

Permalink
service errors updated, wasm errors
Browse files Browse the repository at this point in the history
  • Loading branch information
turbocrime committed Jan 2, 2024
1 parent f5ed0c8 commit ae2b745
Show file tree
Hide file tree
Showing 18 changed files with 184 additions and 93 deletions.
31 changes: 23 additions & 8 deletions packages/router/src/grpc/custody/authorize.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type { Impl } from '.';
import { approverCtx, extLocalCtx, extSessionCtx } from '../../ctx';

import { authorizePlan, generateSpendKey } from '@penumbra-zone/wasm-ts';
import * as wasm from '@penumbra-zone/wasm-ts';

import { Key } from '@penumbra-zone/crypto-web';
import { Box } from '@penumbra-zone/types';

import { ConnectError, Code } from '@connectrpc/connect';

export const authorize: Impl['authorize'] = async (req, ctx) => {
if (!req.plan) throw new Error('No plan included in request');
if (!req.plan) throw new ConnectError('No plan included in request', Code.InvalidArgument);

const approveReq = ctx.values.get(approverCtx);
await approveReq(req);
Expand All @@ -16,16 +18,29 @@ export const authorize: Impl['authorize'] = async (req, ctx) => {
const local = ctx.values.get(extLocalCtx);

const passwordKey = await sess.get('passwordKey');
if (!passwordKey) throw new Error('User must login to extension');
if (!passwordKey) throw new ConnectError('User must login to extension', Code.Unavailable);

const wallets = await local.get('wallets');
const { encryptedSeedPhrase } = wallets[0]!.custody;

const key = await Key.fromJson(passwordKey);
const decryptedSeedPhrase = await key.unseal(Box.fromJson(encryptedSeedPhrase));
if (!decryptedSeedPhrase) throw new Error('Unable to decrypt seed phrase with password');

const spendKey = generateSpendKey(decryptedSeedPhrase);

return { data: authorizePlan(spendKey, req.plan) };
if (!decryptedSeedPhrase)
throw new ConnectError('Unable to decrypt seed phrase with password', Code.Unauthenticated);

let spendKey;
try {
spendKey = wasm.generateSpendKey(decryptedSeedPhrase);
} catch (wasmErr) {
throw new ConnectError('WASM failed to generate spend key', Code.Internal);
}

let data;
try {
data = wasm.authorizePlan(spendKey, req.plan);
} catch (wasmErr) {
throw new ConnectError('WASM failed to authorize plan', Code.Internal);
}

return { data };
};
4 changes: 3 additions & 1 deletion packages/router/src/grpc/custody/export-full-viewing-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import { stringToUint8Array } from '@penumbra-zone/types';

import { FullViewingKey } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1alpha1/keys_pb';

import { ConnectError, Code } from '@connectrpc/connect';

export const exportFullViewingKey: Impl['exportFullViewingKey'] = async (_, ctx) => {
const localExtStorage = ctx.values.get(extLocalCtx);
const wallets = await localExtStorage.get('wallets');
if (!wallets.length) throw new Error('No wallets in storage');
if (!wallets.length) throw new ConnectError('No wallets in storage', Code.FailedPrecondition);
const fullViewingKey = new FullViewingKey({
inner: stringToUint8Array(wallets[0]!.fullViewingKey),
});
Expand Down
13 changes: 11 additions & 2 deletions packages/router/src/grpc/view-protocol-server/address-by-index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import type { Impl } from '.';
import { servicesCtx } from '../../ctx';

import { getAddressByIndex } from '@penumbra-zone/wasm-ts';
import * as wasm from '@penumbra-zone/wasm-ts';

import { ConnectError, Code } from '@connectrpc/connect';

export const addressByIndex: Impl['addressByIndex'] = async (req, ctx) => {
const services = ctx.values.get(servicesCtx);
const {
viewServer: { fullViewingKey },
} = await services.getWalletServices();
const address = getAddressByIndex(fullViewingKey, req.addressIndex?.account ?? 0);

let address;
try {
address = wasm.getAddressByIndex(fullViewingKey, req.addressIndex?.account ?? 0);
} catch (wasmErr) {
throw new ConnectError('WASM failed to get address by index', Code.Internal);
}

return { address };
};
40 changes: 15 additions & 25 deletions packages/router/src/grpc/view-protocol-server/balances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,33 +6,11 @@ import { Base64Str, addLoHi, uint8ArrayToBase64 } from '@penumbra-zone/types';
import { Value } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1alpha1/asset_pb';
import { AddressIndex } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1alpha1/keys_pb';
import { Amount } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/num/v1alpha1/num_pb';
import {
BalancesResponse,
SpendableNoteRecord,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1alpha1/view_pb';
import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1alpha1/view_pb';

type BalancesMap = Record<Base64Str, BalancesResponse>;
type AccountMap = Record<AddressIndex['account'], BalancesMap>;

const initializeProto = (
noteRecord: SpendableNoteRecord,
accountNumber: number,
): BalancesResponse => {
if (!noteRecord.addressIndex?.randomizer) throw new Error('missing addressIndex.randomizer');
if (!noteRecord.note?.value?.assetId) throw new Error('Missing note asset id');

return new BalancesResponse({
account: new AddressIndex({
account: accountNumber,
randomizer: noteRecord.addressIndex.randomizer,
}),
balance: new Value({
amount: new Amount({ hi: 0n, lo: 0n }),
assetId: noteRecord.note.value.assetId,
}),
});
};

// Handles aggregating amounts and filtering by account number/asset id
export const balances: Impl['balances'] = async function* (req, ctx) {
const services = ctx.values.get(servicesCtx);
Expand All @@ -49,12 +27,24 @@ export const balances: Impl['balances'] = async function* (req, ctx) {
accounts[accountNumber] = {};
}

// not throwing ConnectError here, just a normal Error, because these
// failures are truly internal. they did not arise from request input.
const assetIdBytes = noteRecord.note?.value?.assetId?.inner;
if (!assetIdBytes) throw new Error('missing AssetId bytes');

const assetId = uint8ArrayToBase64(assetIdBytes);
if (!accounts[accountNumber]![assetId]) {
accounts[accountNumber]![assetId] = initializeProto(noteRecord, accountNumber);
if (!noteRecord.addressIndex?.randomizer) throw new Error('missing addressIndex.randomizer');
if (!noteRecord.note?.value?.assetId) throw new Error('Missing note asset id');
accounts[accountNumber]![assetId] = new BalancesResponse({
account: new AddressIndex({
account: accountNumber,
randomizer: noteRecord.addressIndex.randomizer,
}),
balance: new Value({
amount: new Amount({ hi: 0n, lo: 0n }),
assetId: noteRecord.note.value.assetId,
}),
});
}

// Many type overrides, but initialization above guarantees presence
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import type { Impl } from '.';
import { servicesCtx } from '../../ctx';

import { encodeTx } from '@penumbra-zone/wasm-ts';
import * as wasm from '@penumbra-zone/wasm-ts';

import { NoteSource } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/chain/v1alpha1/chain_pb';
import { SpendableNoteRecord } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1alpha1/view_pb';

import { ConnectError, Code } from '@connectrpc/connect';

export const broadcastTransaction: Impl['broadcastTransaction'] = async (req, ctx) => {
const services = ctx.values.get(servicesCtx);
const { tendermint } = services.querier;
const { indexedDb } = await services.getWalletServices();
if (!req.transaction) throw new Error('No transaction provided in request');

const encodedTx = encodeTx(req.transaction);
if (!req.transaction)
throw new ConnectError('No transaction provided in request', Code.InvalidArgument);

let encodedTx;
try {
encodedTx = wasm.encodeTx(req.transaction);
} catch (wasmErr) {
throw new ConnectError('WASM failed to encode transaction', Code.Internal);
}

// start subscription early to prevent race condition
const subscription = indexedDb.subscribe('SPENDABLE_NOTES');
Expand Down
13 changes: 11 additions & 2 deletions packages/router/src/grpc/view-protocol-server/ephemeral-address.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import type { Impl } from '.';
import { servicesCtx } from '../../ctx';

import { getEphemeralByIndex } from '@penumbra-zone/wasm-ts';
import * as wasm from '@penumbra-zone/wasm-ts';

import { ConnectError, Code } from '@connectrpc/connect';

export const ephemeralAddress: Impl['ephemeralAddress'] = async (req, ctx) => {
const services = ctx.values.get(servicesCtx);
const {
viewServer: { fullViewingKey },
} = await services.getWalletServices();
const address = getEphemeralByIndex(fullViewingKey, req.addressIndex?.account ?? 0);

let address;
try {
address = wasm.getEphemeralByIndex(fullViewingKey, req.addressIndex?.account ?? 0);
} catch (wasmErr) {
throw new ConnectError('WASM failed to generate address', Code.Internal);
}

return { address };
};
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { Impl } from '.';
import { servicesCtx } from '../../ctx';

import { ConnectError, Code } from '@connectrpc/connect';

export const fMDParameters: Impl['fMDParameters'] = async (_, ctx) => {
const services = ctx.values.get(servicesCtx);
const { indexedDb } = await services.getWalletServices();
const parameters = await indexedDb.getFmdParams();
if (!parameters) throw new Error('No FMD parameters');
if (!parameters) throw new ConnectError('No FMD parameters', Code.FailedPrecondition);
return { parameters };
};
21 changes: 17 additions & 4 deletions packages/router/src/grpc/view-protocol-server/index-by-address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,31 @@ import type { Impl } from '.';
import { servicesCtx } from '../../ctx';

import { bech32Address } from '@penumbra-zone/types';
import { isControlledAddress } from '@penumbra-zone/wasm-ts';
import * as wasm from '@penumbra-zone/wasm-ts';

import { ConnectError, Code } from '@connectrpc/connect';

export const indexByAddress: Impl['indexByAddress'] = async (req, ctx) => {
if (!req.address) throw new Error('no address given in request');
if (!req.address) throw new ConnectError('no address given in request', Code.InvalidArgument);
const services = ctx.values.get(servicesCtx);
const {
viewServer: { fullViewingKey },
} = await services.getWalletServices();

const address = bech32Address(req.address);
const addressIndex = isControlledAddress(fullViewingKey, address);
if (!addressIndex) throw new Error('Address is not controlled by view service full viewing key');

let addressIndex;
try {
addressIndex = wasm.isControlledAddress(fullViewingKey, address);
} catch (wasmErr) {
throw new ConnectError('WASM failed to get address index', Code.Internal);
}

if (!addressIndex)
throw new ConnectError(
'Address is not controlled by view service full viewing key',
Code.Unauthenticated,
);

return { addressIndex };
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ import {
SpendableNoteRecord,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1alpha1/view_pb';

import { ConnectError, Code } from '@connectrpc/connect';

export const noteByCommitment: Impl['noteByCommitment'] = async (req, ctx) => {
const services = ctx.values.get(servicesCtx);
const { indexedDb } = await services.getWalletServices();
if (!req.noteCommitment) throw new Error('Missing note commitment in request');
if (!req.noteCommitment)
throw new ConnectError('Missing note commitment in request', Code.InvalidArgument);

const noteByCommitment = await indexedDb.getNoteByCommitment(req.noteCommitment);
if (noteByCommitment) return { spendableNote: noteByCommitment };
if (!req.awaitDetection) throw new Error('Note not found');
if (!req.awaitDetection) throw new ConnectError('Note not found', Code.NotFound);

// Wait until our DB encounters a new note with this commitment
const response = new NoteByCommitmentResponse();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
import type { Impl } from '.';
import { servicesCtx, hasWalletCtx } from '../../ctx';

import { ConnectError, Code } from '@connectrpc/connect';

const watchStream = async <U>(
subscription: AsyncGenerator<U>,
test: (x: U) => boolean,
Expand All @@ -18,7 +20,7 @@ export const nullifierStatus: Impl['nullifierStatus'] = async (req, ctx) => {
await hasWallet(req.walletId);

const { nullifier } = req;
if (!nullifier) throw new Error('No nullifier passed');
if (!nullifier) throw new ConnectError('No nullifier passed', Code.InvalidArgument);

const services = ctx.values.get(servicesCtx);
const { indexedDb } = await services.getWalletServices();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import { servicesCtx } from '../../ctx';

import { SwapRecord } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1alpha1/view_pb';

import { ConnectError, Code } from '@connectrpc/connect';

export const swapByCommitment: Impl['swapByCommitment'] = async (req, ctx) => {
const services = ctx.values.get(servicesCtx);
const { indexedDb } = await services.getWalletServices();
const { swapCommitment } = req;
if (!swapCommitment) throw new Error('Missing swap commitment in request');
if (!swapCommitment)
throw new ConnectError('Missing swap commitment in request', Code.InvalidArgument);

const swap = await indexedDb.getSwapByCommitment(swapCommitment);
if (swap) return { swap };
Expand All @@ -19,5 +22,5 @@ export const swapByCommitment: Impl['swapByCommitment'] = async (req, ctx) => {
}
}

throw new Error('Swap not found');
throw new ConnectError('Swap not found', Code.NotFound);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { servicesCtx } from '../../ctx';

import { NoteSource } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/chain/v1alpha1/chain_pb';

import { ConnectError, Code } from '@connectrpc/connect';

export const transactionInfoByHash: Impl['transactionInfoByHash'] = async (req, ctx) => {
const services = ctx.values.get(servicesCtx);
const { indexedDb } = await services.getWalletServices();
if (!req.id) throw new Error('Missing transaction ID in request');
if (!req.id) throw new ConnectError('Missing transaction ID in request', Code.InvalidArgument);

const txInfo = await indexedDb.getTransaction(new NoteSource({ inner: req.id.hash }));
if (!txInfo) throw new Error('Transaction not found');
if (!txInfo) throw new ConnectError('Transaction not found', Code.NotFound);

return { txInfo };
};
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { Impl } from '.';
import { servicesCtx } from '../../ctx';

import { getAddressByIndex, TxPlanner } from '@penumbra-zone/wasm-ts';
import * as wasm from '@penumbra-zone/wasm-ts';

import { AddressIndex } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1alpha1/keys_pb';

import { ConnectError, Code } from '@connectrpc/connect';

export const transactionPlanner: Impl['transactionPlanner'] = async (req, ctx) => {
const services = ctx.values.get(servicesCtx);
const {
Expand All @@ -13,13 +15,19 @@ export const transactionPlanner: Impl['transactionPlanner'] = async (req, ctx) =
} = await services.getWalletServices();
const chainParams = await services.querier.app.chainParams();
const fmdParams = await indexedDb.getFmdParams();
if (!fmdParams) throw new Error('Fmd Params not in indexeddb');

const planner = await TxPlanner.initialize({
idbConstants: indexedDb.constants(),
chainParams,
fmdParams,
});
if (!fmdParams) throw new ConnectError('Fmd Params not in indexeddb', Code.FailedPrecondition);
const idbConstants = indexedDb.constants();

let planner;
try {
planner = await wasm.TxPlanner.initialize({
idbConstants,
chainParams,
fmdParams,
});
} catch (wasmErr) {
throw new ConnectError('WASM failed to initialize transaction planner', Code.Internal);
}

if (req.expiryHeight) planner.expiryHeight(req.expiryHeight);
if (req.memo) planner.memo(req.memo);
Expand All @@ -33,7 +41,12 @@ export const transactionPlanner: Impl['transactionPlanner'] = async (req, ctx) =
const source = req.source ?? new AddressIndex({ account: 0 });

// If there are any balances left over, refund back to source. Default to account 0.
const refundAddr = getAddressByIndex(fullViewingKey, source.account);
let refundAddr;
try {
refundAddr = wasm.getAddressByIndex(fullViewingKey, source.account);
} catch (wasmErr) {
throw new ConnectError('WASM failed to get address by index', Code.Internal);
}

const plan = await planner.plan(refundAddr, source);
return { plan };
Expand Down
Loading

0 comments on commit ae2b745

Please sign in to comment.