Skip to content

Commit

Permalink
Merge pull request #59 from NillionNetwork/feat/unwrap-unexpected-error
Browse files Browse the repository at this point in the history
feat: unwrap cause from UnexpectedException
  • Loading branch information
pablojhl authored Jan 13, 2025
2 parents 25c054d + b7a15fb commit a4ab2b7
Show file tree
Hide file tree
Showing 16 changed files with 149 additions and 57 deletions.
2 changes: 1 addition & 1 deletion client-vms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@nillion/client-vms",
"license": "MIT",
"author": "[email protected]",
"version": "0.3.0-rc.0",
"version": "0.3.0-rc.1",
"repository": "https://github.com/NillionNetwork/client-ts",
"type": "module",
"exports": {
Expand Down
95 changes: 69 additions & 26 deletions client-vms/src/payment/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SigningStargateClient } from "@cosmjs/stargate";
import { sha256 } from "@noble/hashes/sha2";
import { randomBytes } from "@noble/hashes/utils";
import { Effect as E, pipe } from "effect";
import { UnknownException } from "effect/Cause";
import { z } from "zod";
import {
type MsgPayFor,
Expand All @@ -29,6 +30,7 @@ import { Log } from "#/logger";
import { UserId } from "#/types";
import { GrpcClient } from "#/types/grpc";
import { Quote } from "#/types/types";
import { unwrapExceptionCause } from "#/util";
import {
NilChainAddress,
NilChainProtobufTypeUrl,
Expand Down Expand Up @@ -77,18 +79,29 @@ export class PaymentClient {
}

async quote(request: PriceQuoteRequest): Promise<Quote> {
const signed = await this.leader.priceQuote(request);
const quotePb = fromBinary(PriceQuoteSchema, signed.quote);
const quote = Quote.parse(
{ ...quotePb, request, signed },
{ path: ["client.quote"] },
);
Log(
"Quoted %s unil for %s",
quote.fees.total.toString(),
request.operation.case,
return pipe(
E.tryPromise(() => this.leader.priceQuote(request)),
E.map((signed) => {
const quotePb = fromBinary(PriceQuoteSchema, signed.quote);
return Quote.parse(
{ ...quotePb, request, signed },
{ path: ["client.quote"] },
);
}),
E.catchAll(unwrapExceptionCause),
E.tapBoth({
onFailure: (e) => E.sync(() => Log("Quote request failed: %O", e)),
onSuccess: (quote) =>
E.sync(() =>
Log(
"Quoted %s unil for %s",
quote.fees.total.toString(),
request.operation.case,
),
),
}),
E.runPromise,
);
return quote;
}

async payOnChain(
Expand All @@ -104,14 +117,23 @@ export class PaymentClient {
resource: quote.nonce,
amount: [{ denom: NilToken.Unil, amount }],
});
const result = await this.chain.signAndBroadcast(
this.address,
[{ typeUrl: NilChainProtobufTypeUrl, value }],
"auto",
return pipe(
E.tryPromise(() =>
this.chain.signAndBroadcast(
this.address,
[{ typeUrl: NilChainProtobufTypeUrl, value }],
"auto",
),
),
E.map((result) => TxHash.parse(result.transactionHash)),
E.catchAll(unwrapExceptionCause),
E.tapBoth({
onSuccess: (hash) =>
E.sync(() => Log("Paid %d unil hash: %s", amount, hash)),
onFailure: (e) => E.sync(() => Log("Pay failed: %O", e)),
}),
E.runPromise,
);
const hash = TxHash.parse(result.transactionHash);
Log("Paid %d unil hash: %s", amount, hash);
return hash;
}

async validate(
Expand All @@ -127,6 +149,9 @@ export class PaymentClient {
Log("Validated payment with cluster");
return receipt;
} catch (error) {
if (error instanceof UnknownException) {
throw unwrapExceptionCause(error);
}
if (
error instanceof ConnectError &&
error.code === Code.FailedPrecondition &&
Expand All @@ -143,18 +168,35 @@ export class PaymentClient {
}

async accountBalance(): Promise<AccountBalanceResponse> {
const accountBalance = await this.leader.accountBalance({});
Log("Account balance: %d unil", accountBalance.balance);
return accountBalance;
return pipe(
E.tryPromise(() => this.leader.accountBalance({})),
E.catchAll(unwrapExceptionCause),
E.tapBoth({
onSuccess: (accountBalance) =>
E.sync(() => Log("Account balance: %d unil", accountBalance.balance)),
onFailure: (e) => E.sync(() => Log("Pay failed: %O", e)),
}),
E.runPromise,
);
}

async paymentsConfig(): Promise<PaymentsConfigResponse> {
const paymentsConfig = await this.leader.paymentsConfig({});
Log(
"Minimum add unil amount: %d unil",
paymentsConfig.minimumAddFundsPayment,
return pipe(
E.tryPromise(() => this.leader.paymentsConfig({})),
E.catchAll(unwrapExceptionCause),
E.tapBoth({
onSuccess: (paymentsConfig) =>
E.sync(() =>
Log(
"Minimum add unil amount: %d unil",
paymentsConfig.minimumAddFundsPayment,
),
),
onFailure: (e) =>
E.sync(() => Log("Payment config request failed: %O", e)),
}),
E.runPromise,
);
return paymentsConfig;
}

async addFunds(unilAmount: bigint): Promise<Empty> {
Expand Down Expand Up @@ -191,6 +233,7 @@ export class PaymentClient {
}),
),
E.andThen(this.leader.addFunds),
E.catchAll(unwrapExceptionCause),
E.runPromise,
);
}
Expand Down
10 changes: 10 additions & 0 deletions client-vms/src/util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Effect as E, pipe } from "effect";
import { UnknownException } from "effect/Cause";
import type { Effect } from "effect/Effect";

export const collapse = <T>(list: T[]): E.Effect<T, UnknownException> => {
return pipe(
Expand Down Expand Up @@ -28,3 +29,12 @@ export function assertIsDefined<T>(
throw new Error(`Expected ${name} to be defined but got ${value}`);
}
}

export function unwrapExceptionCause(
error: UnknownException | Error,
): Effect<never, unknown, never> {
if (error.cause === null || error.cause === undefined) {
return E.fail(error);
}
return E.fail(error.cause);
}
41 changes: 25 additions & 16 deletions client-vms/src/vm/builder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { createClient } from "@connectrpc/connect";
import { type Client, createClient } from "@connectrpc/connect";
import { createGrpcWebTransport } from "@connectrpc/connect-web";
import type { OfflineSigner } from "@cosmjs/proto-signing";
import { SecretMasker } from "@nillion/client-wasm";
import { Effect as E, pipe } from "effect";
import { z } from "zod";
import { TokenAuthManager, createAuthInterceptor } from "#/auth";
import {
Expand All @@ -14,7 +15,7 @@ import { Log } from "#/logger";
import { PaymentClientBuilder, PaymentMode } from "#/payment";
import { PartyId, UserId } from "#/types";
import { OfflineSignerSchema } from "#/types/grpc";
import { assertIsDefined } from "#/util";
import { assertIsDefined, unwrapExceptionCause } from "#/util";
import { VmClient, VmClientConfig } from "#/vm/client";

export const VmClientBuilderConfig = z.object({
Expand Down Expand Up @@ -221,20 +222,30 @@ export class VmClientBuilder {
}
}

function createMembershipClient(
bootnodeUrl: string,
): Client<typeof Membership> {
return createClient(
Membership,
createGrpcWebTransport({
baseUrl: bootnodeUrl,
useBinaryFormat: true,
}),
);
}

/**
* Fetches cluster details from the specified bootnode Url.
*
* @param {string} bootnodeUrl - The Url of the bootnode to query.
* @returns {Promise<Cluster>} A promise that resolves with the cluster details.
*/
export const fetchClusterDetails = (bootnodeUrl: string): Promise<Cluster> => {
return createClient(
Membership,
createGrpcWebTransport({
baseUrl: bootnodeUrl,
useBinaryFormat: true,
}),
).cluster({});
return pipe(
E.tryPromise(() => createMembershipClient(bootnodeUrl).cluster({})),
E.catchAll(unwrapExceptionCause),
E.runPromise,
);
};

/**
Expand All @@ -244,11 +255,9 @@ export const fetchClusterDetails = (bootnodeUrl: string): Promise<Cluster> => {
* @returns {Promise<Cluster>} A promise that resolves with the node version.
*/
export const fetchNodeVersion = (bootnodeUrl: string): Promise<NodeVersion> => {
return createClient(
Membership,
createGrpcWebTransport({
baseUrl: bootnodeUrl,
useBinaryFormat: true,
}),
).nodeVersion({});
return pipe(
E.tryPromise(() => createMembershipClient(bootnodeUrl).nodeVersion({})),
E.catchAll(unwrapExceptionCause),
E.runPromise,
);
};
3 changes: 2 additions & 1 deletion client-vms/src/vm/operation/delete-values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
import { Values } from "#/gen-proto/nillion/values/v1/service_pb";
import { Log } from "#/logger";
import { type PartyId, Uuid } from "#/types/types";
import { collapse } from "#/util";
import { collapse, unwrapExceptionCause } from "#/util";
import type { VmClient } from "#/vm/client";
import type { Operation } from "#/vm/operation/operation";
import { retryGrpcRequestIfRecoverable } from "#/vm/operation/retry-client";
Expand Down Expand Up @@ -48,6 +48,7 @@ export class DeleteValues implements Operation<Uuid> {
E.all(effects, { concurrency: this.config.vm.nodes.length }),
),
E.flatMap(collapse),
E.catchAll(unwrapExceptionCause),
E.tapBoth({
onFailure: (e) => E.sync(() => Log("Values delete failed: %O", e)),
onSuccess: (id) => E.sync(() => Log(`Values deleted: ${id}`)),
Expand Down
3 changes: 2 additions & 1 deletion client-vms/src/vm/operation/invoke-compute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
Uuid,
} from "#/types/types";
import type { UserId } from "#/types/user-id";
import { collapse } from "#/util";
import { collapse, unwrapExceptionCause } from "#/util";
import type { VmClient } from "#/vm/client";
import type { Operation } from "#/vm/operation/operation";
import { retryGrpcRequestIfRecoverable } from "#/vm/operation/retry-client";
Expand Down Expand Up @@ -70,6 +70,7 @@ export class InvokeCompute implements Operation<Uuid> {
E.all(effects, { concurrency: this.config.vm.nodes.length }),
),
E.flatMap(collapse),
E.catchAll(unwrapExceptionCause),
E.tapBoth({
onFailure: (e) => E.sync(() => Log("Invoke compute failed: %O", e)),
onSuccess: (id) => E.sync(() => Log(`Invoke compute: ${id}`)),
Expand Down
3 changes: 2 additions & 1 deletion client-vms/src/vm/operation/overwrite-permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Permissions as PermissionsService } from "#/gen-proto/nillion/permissio
import { Log } from "#/logger";
import { type PartyId, Uuid } from "#/types/types";
import type { ValuesPermissions } from "#/types/values-permissions";
import { collapse } from "#/util";
import { collapse, unwrapExceptionCause } from "#/util";
import type { VmClient } from "#/vm/client";
import type { Operation } from "#/vm/operation/operation";
import { retryGrpcRequestIfRecoverable } from "#/vm/operation/retry-client";
Expand Down Expand Up @@ -55,6 +55,7 @@ export class OverwritePermissions implements Operation<ValuesPermissions> {
E.all(effects, { concurrency: this.config.vm.nodes.length }),
),
E.flatMap(collapse),
E.catchAll(unwrapExceptionCause),
E.tapBoth({
onFailure: (e) =>
E.sync(() => Log("Overwrite permissions failed: %O", e)),
Expand Down
2 changes: 2 additions & 0 deletions client-vms/src/vm/operation/query-pool-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { LeaderQueries } from "#/gen-proto/nillion/leader_queries/v1/service_pb"
import { PriceQuoteRequestSchema } from "#/gen-proto/nillion/payments/v1/quote_pb";
import type { SignedReceipt } from "#/gen-proto/nillion/payments/v1/receipt_pb";
import { Log } from "#/logger";
import { unwrapExceptionCause } from "#/util";
import type { VmClient } from "#/vm/client";
import type { Operation } from "#/vm/operation/operation";
import { retryGrpcRequestIfRecoverable } from "#/vm/operation/retry-client";
Expand Down Expand Up @@ -53,6 +54,7 @@ export class QueryPoolStatus implements Operation<PoolStatus> {
),
),
E.flatMap((response) => E.try(() => PoolStatus.parse(response))),
E.catchAll(unwrapExceptionCause),
E.tapBoth({
onFailure: (e) => E.sync(() => Log("Query pool status failed: %O", e)),
onSuccess: (status) => E.sync(() => Log("Pool status: %O", status)),
Expand Down
2 changes: 2 additions & 0 deletions client-vms/src/vm/operation/retrieve-compute-result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { Compute } from "#/gen-proto/nillion/compute/v1/service_pb";
import { Log } from "#/logger";
import { NadaValuesRecord, type PartyId, Uuid } from "#/types/types";
import { unwrapExceptionCause } from "#/util";
import type { VmClient } from "#/vm/client";
import type { Operation } from "#/vm/operation/operation";
import { retryGrpcRequestIfRecoverable } from "#/vm/operation/retry-client";
Expand Down Expand Up @@ -55,6 +56,7 @@ export class RetrieveComputeResult implements Operation<NadaValuesRecord> {
const record = values.to_record() as unknown;
return NadaValuesRecord.parse(record);
}),
E.catchAll(unwrapExceptionCause),
E.tapBoth({
onFailure: (e) =>
E.sync(() => Log("Retrieve compute results failed: %O", e)),
Expand Down
3 changes: 2 additions & 1 deletion client-vms/src/vm/operation/retrieve-permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { Permissions as PermissionsService } from "#/gen-proto/nillion/permissio
import { Log } from "#/logger";
import { type PartyId, Uuid } from "#/types/types";
import { ValuesPermissions } from "#/types/values-permissions";
import { collapse } from "#/util";
import { collapse, unwrapExceptionCause } from "#/util";
import type { VmClient } from "#/vm/client";
import type { Operation } from "#/vm/operation/operation";
import { retryGrpcRequestIfRecoverable } from "#/vm/operation/retry-client";
Expand Down Expand Up @@ -54,6 +54,7 @@ export class RetrievePermissions implements Operation<ValuesPermissions> {
E.all(effects, { concurrency: this.config.vm.nodes.length }),
),
E.flatMap(collapse),
E.catchAll(unwrapExceptionCause),
E.tapBoth({
onFailure: (e) =>
E.sync(() => Log("Retrieve permissions failed: %O", e)),
Expand Down
2 changes: 2 additions & 0 deletions client-vms/src/vm/operation/retrieve-values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { Values } from "#/gen-proto/nillion/values/v1/service_pb";
import { Log } from "#/logger";
import { NadaValuesRecord, type PartyId, Uuid } from "#/types/types";
import { unwrapExceptionCause } from "#/util";
import type { VmClient } from "#/vm/client";
import type { Operation } from "#/vm/operation/operation";
import { retryGrpcRequestIfRecoverable } from "#/vm/operation/retry-client";
Expand Down Expand Up @@ -55,6 +56,7 @@ export class RetrieveValues implements Operation<NadaValuesRecord> {
const record = values.to_record() as unknown;
return NadaValuesRecord.parse(record);
}),
E.catchAll(unwrapExceptionCause),
E.tapBoth({
onFailure: (e) => E.sync(() => Log("Retrieve values failed: %O", e)),
onSuccess: (data) => E.sync(() => Log("Retrieved values: %O", data)),
Expand Down
Loading

0 comments on commit a4ab2b7

Please sign in to comment.