Skip to content
Draft
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
8 changes: 5 additions & 3 deletions .github/scripts/install-evm-key-scan-hook.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ validate_policy() {
exit 2
}
[[ -d "$policy_dir/.git" ]] || return 1
origin=$(git -C "$policy_dir" remote get-url origin) || {
origin=$(env -u GIT_DIR -u GIT_WORK_TREE git -C "$policy_dir" remote get-url origin) || {
printf 'Refusing unreadable policy cache: %s\n' "$policy_dir" >&2
exit 2
}
Expand All @@ -44,11 +44,11 @@ validate_policy() {
exit 2
;;
esac
[[ "$(git -C "$policy_dir" rev-parse HEAD)" == "$CENTRAL_POLICY_SHA" ]] || {
[[ "$(env -u GIT_DIR -u GIT_WORK_TREE git -C "$policy_dir" rev-parse HEAD)" == "$CENTRAL_POLICY_SHA" ]] || {
printf 'Refusing stale policy cache: %s\n' "$policy_dir" >&2
exit 2
}
[[ -z "$(git -C "$policy_dir" status --porcelain --untracked-files=all -- ':!/.tools')" ]] || {
[[ -z "$(env -u GIT_DIR -u GIT_WORK_TREE git -C "$policy_dir" status --porcelain --untracked-files=all -- ':!/.tools')" ]] || {
printf 'Refusing modified policy cache: %s\n' "$policy_dir" >&2
exit 2
}
Expand Down Expand Up @@ -102,6 +102,8 @@ if [[ "$action" == run ]]; then
exit 2
}
exec env \
-u GIT_DIR \
-u GIT_WORK_TREE \
VANA_SECRET_SCAN_HOME="$policy_dir" \
VANA_SECRET_SCAN_EXPECTED_SHA="$CENTRAL_POLICY_SHA" \
"$policy_dir/hooks/pre-push" "$@"
Expand Down
30 changes: 29 additions & 1 deletion packages/vana-sdk/scripts/validate-package-imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ function validateTypeScriptConsumer(consumerDir: string): void {
join(consumerDir, "index.ts"),
[
'import { createSessionRelayBuilderClient, SessionRelayError, type SessionRelayInitResult } from "@opendatalabs/vana-sdk/session-relay";',
'import { buildEscrowPaymentHeader, type EscrowPaymentConfig, type EscrowPaymentHeaderConfig, type SignTypedDataFn } from "@opendatalabs/vana-sdk/server";',
'import { buildEscrowPaymentHeader, type EscrowPaymentConfig, type EscrowPaymentHeaderConfig, type PersonalServerPaymentOperation, type SignTypedDataFn } from "@opendatalabs/vana-sdk/server";',
'import { buildWithdrawAuthorizationTypedData, createEscrowGatewayClient, EscrowWithdrawalLifecycleError, EscrowWithdrawalRejectionError, type EscrowWithdrawalResult } from "@opendatalabs/vana-sdk/node";',
'import { buildEscrowPaymentHeader as buildDirectEscrowPaymentHeader } from "@opendatalabs/vana-sdk/direct/escrow-payment";',
'import { readPersonalServerData } from "@opendatalabs/vana-sdk/direct/personal-server-read";',
"",
Expand Down Expand Up @@ -123,6 +124,14 @@ function validateTypeScriptConsumer(consumerDir: string): void {
" legacyConfig;",
"void headerOnlyInput;",
"void legacyInput;",
"const paymentOperation = {} as PersonalServerPaymentOperation;",
"const withdrawalResult = {} as EscrowWithdrawalResult;",
"void paymentOperation;",
"void withdrawalResult;",
"void buildWithdrawAuthorizationTypedData;",
"void createEscrowGatewayClient;",
"void EscrowWithdrawalLifecycleError;",
"void EscrowWithdrawalRejectionError;",
"void buildDirectEscrowPaymentHeader;",
"void readPersonalServerData;",
"",
Expand Down Expand Up @@ -173,6 +182,25 @@ try {
console.log(`✓ ${specifier}`);
}

run(
"node",
[
"--input-type=module",
"-e",
'const sdk = await import("@opendatalabs/vana-sdk/server"); if (typeof sdk.buildEscrowPaymentHeader !== "function") throw new Error("Server entry point is missing buildEscrowPaymentHeader");',
],
consumerDir,
);
run(
"node",
[
"--input-type=module",
"-e",
'const sdk = await import("@opendatalabs/vana-sdk/node"); for (const name of ["buildWithdrawAuthorizationTypedData", "createEscrowGatewayClient", "EscrowWithdrawalLifecycleError", "EscrowWithdrawalRejectionError"]) if (!(name in sdk)) throw new Error(`Node entry point is missing ${name}`);',
],
consumerDir,
);

for (const specifier of browserBlockedImports) {
validateBrowserBlockedImport(specifier, consumerDir);
}
Expand Down
6 changes: 0 additions & 6 deletions packages/vana-sdk/src/direct/controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,9 +323,6 @@ function mockEscrowConfig(
): DirectEscrowConfig {
return {
client: {
submitDeposit: vi.fn(),
getEscrowBalance: vi.fn(),
syncEscrowBalance: vi.fn(),
payForOp,
},
escrowContract: "0x000000000000000000000000000000000000dEaD",
Expand Down Expand Up @@ -600,9 +597,6 @@ function makeControllerWithPaymentCapture(

const spyPayForOp = vi.fn(async () => payResultFixture());
const spyClient = {
submitDeposit: vi.fn(),
getEscrowBalance: vi.fn(),
syncEscrowBalance: vi.fn(),
payForOp: spyPayForOp,
};

Expand Down
6 changes: 0 additions & 6 deletions packages/vana-sdk/src/direct/escrow-payment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,6 @@ describe("authorizeGrantPayment", () => {
const signTypedData = vi.fn(async () => "0xsig" as `0x${string}`);
const cfg: EscrowPaymentConfig = {
client: {
submitDeposit: vi.fn(),
getEscrowBalance: vi.fn(),
syncEscrowBalance: vi.fn(),
payForOp,
},
escrowContract: ESCROW,
Expand Down Expand Up @@ -262,9 +259,6 @@ describe("generic escrow payment operations", () => {
const signTypedData = vi.fn(async () => "0xsig" as `0x${string}`);
const cfg: EscrowPaymentConfig = {
client: {
submitDeposit: vi.fn(),
getEscrowBalance: vi.fn(),
syncEscrowBalance: vi.fn(),
payForOp,
},
escrowContract: ESCROW,
Expand Down
4 changes: 2 additions & 2 deletions packages/vana-sdk/src/direct/escrow-payment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
NATIVE_ASSET_ADDRESS,
genericPaymentDomain,
type EscrowAccessRecord,
type EscrowGatewayClient,
type EscrowPaymentClient,
type EscrowPayResult,
type PaymentBreakdown,
} from "../protocol/escrow";
Expand Down Expand Up @@ -115,7 +115,7 @@ export interface EscrowPaymentHeaderConfig {
*/
export interface EscrowPaymentConfig extends EscrowPaymentHeaderConfig {
/** Client for the gateway escrow endpoints (`/v1/escrow/*`). */
client: EscrowGatewayClient;
client: EscrowPaymentClient;
}

/** Map the gateway {@link PaymentBreakdown} into the public {@link DirectFeeBreakdown}. */
Expand Down
3 changes: 0 additions & 3 deletions packages/vana-sdk/src/direct/personal-server-read.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,6 @@ function payResultFixture(opId: string) {
function mockEscrow(payForOp = vi.fn()): EscrowPaymentConfig {
return {
client: {
submitDeposit: vi.fn(),
getEscrowBalance: vi.fn(),
syncEscrowBalance: vi.fn(),
payForOp,
},
escrowContract: "0x000000000000000000000000000000000000dEaD",
Expand Down
17 changes: 17 additions & 0 deletions packages/vana-sdk/src/index.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,15 @@ export {
serverRegistrationDomain,
builderRegistrationDomain,
escrowPaymentDomain,
withdrawAuthorizationDomain,
buildWithdrawAuthorizationTypedData,
GRANT_REGISTRATION_TYPES,
GRANT_REVOCATION_TYPES,
SERVER_REGISTRATION_TYPES,
BUILDER_REGISTRATION_TYPES,
ADD_DATA_TYPES,
RECORD_DATA_ACCESS_TYPES,
WITHDRAW_AUTHORIZATION_TYPES,
type DataPortabilityContracts,
type DataPortabilityGatewayConfig,
type GrantRegistrationMessage,
Expand All @@ -164,6 +167,7 @@ export {
type BuilderRegistrationMessage,
type AddDataMessage,
type RecordDataAccessMessage,
type WithdrawAuthorizationMessage,
} from "./protocol/eip712";
export {
PERSONAL_SERVER_REGISTRATION_DEFAULT_CHAIN_ID,
Expand Down Expand Up @@ -326,8 +330,21 @@ export {
type DepositSubmissionResult,
type PaymentBreakdown,
type EscrowPayResult,
EscrowWithdrawalLifecycleError,
EscrowWithdrawalRejectionError,
type EscrowPaymentClient,
type EscrowWithdrawalFailureResult,
type EscrowWithdrawalRejectedResult,
type EscrowWithdrawalRejectionCode,
type EscrowWithdrawalSubmittedResult,
type EscrowWithdrawalSubmittedWithoutTransaction,
type EscrowWithdrawalSubmittedWithTransaction,
type EscrowWithdrawalSettledResult,
type EscrowWithdrawalResult,
type SubmitDepositParams,
type PayForOpParams,
type WithdrawFromEscrowParams,
type WithdrawNonceResponse,
type EscrowGatewayClient,
type SubmittedDepositEntry,
type FinalizedDepositEntry,
Expand Down
17 changes: 17 additions & 0 deletions packages/vana-sdk/src/index.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,15 @@ export {
serverRegistrationDomain,
builderRegistrationDomain,
escrowPaymentDomain,
withdrawAuthorizationDomain,
buildWithdrawAuthorizationTypedData,
GRANT_REGISTRATION_TYPES,
GRANT_REVOCATION_TYPES,
SERVER_REGISTRATION_TYPES,
BUILDER_REGISTRATION_TYPES,
ADD_DATA_TYPES,
RECORD_DATA_ACCESS_TYPES,
WITHDRAW_AUTHORIZATION_TYPES,
type DataPortabilityContracts,
type DataPortabilityGatewayConfig,
type GrantRegistrationMessage,
Expand All @@ -164,6 +167,7 @@ export {
type BuilderRegistrationMessage,
type AddDataMessage,
type RecordDataAccessMessage,
type WithdrawAuthorizationMessage,
} from "./protocol/eip712";
export {
PERSONAL_SERVER_REGISTRATION_DEFAULT_CHAIN_ID,
Expand Down Expand Up @@ -326,8 +330,21 @@ export {
type DepositSubmissionResult,
type PaymentBreakdown,
type EscrowPayResult,
EscrowWithdrawalLifecycleError,
EscrowWithdrawalRejectionError,
type EscrowPaymentClient,
type EscrowWithdrawalFailureResult,
type EscrowWithdrawalRejectedResult,
type EscrowWithdrawalRejectionCode,
type EscrowWithdrawalSubmittedResult,
type EscrowWithdrawalSubmittedWithoutTransaction,
type EscrowWithdrawalSubmittedWithTransaction,
type EscrowWithdrawalSettledResult,
type EscrowWithdrawalResult,
type SubmitDepositParams,
type PayForOpParams,
type WithdrawFromEscrowParams,
type WithdrawNonceResponse,
type EscrowGatewayClient,
type SubmittedDepositEntry,
type FinalizedDepositEntry,
Expand Down
37 changes: 37 additions & 0 deletions packages/vana-sdk/src/protocol/eip712.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { describe, expect, it } from "vitest";
import { recoverTypedDataAddress } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import {
ADD_DATA_TYPES,
BUILDER_REGISTRATION_TYPES,
Expand All @@ -8,12 +10,15 @@ import {
NATIVE_VANA_ASSET,
RECORD_DATA_ACCESS_TYPES,
SERVER_REGISTRATION_TYPES,
WITHDRAW_AUTHORIZATION_TYPES,
builderRegistrationDomain,
buildWithdrawAuthorizationTypedData,
dataRegistryDomain,
escrowPaymentDomain,
grantRegistrationDomain,
grantRevocationDomain,
serverRegistrationDomain,
withdrawAuthorizationDomain,
type DataPortabilityGatewayConfig,
} from "./eip712";

Expand Down Expand Up @@ -52,6 +57,9 @@ describe("Data Portability EIP-712 helpers", () => {
expect(escrowPaymentDomain(CONFIG)).toMatchObject({
verifyingContract: CONFIG.contracts.dataPortabilityEscrow,
});
expect(withdrawAuthorizationDomain(CONFIG)).toEqual(
escrowPaymentDomain(CONFIG),
);
});

it("exposes the native VANA asset sentinel", () => {
Expand Down Expand Up @@ -93,6 +101,13 @@ describe("Data Portability EIP-712 helpers", () => {
{ name: "amount", type: "uint256" },
{ name: "paymentNonce", type: "uint256" },
]);
expect(WITHDRAW_AUTHORIZATION_TYPES.WithdrawAuthorization).toEqual([
{ name: "account", type: "address" },
{ name: "asset", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "withdrawNonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
]);
expect(ADD_DATA_TYPES.AddData).toEqual([
{ name: "ownerAddress", type: "address" },
{ name: "scope", type: "string" },
Expand All @@ -108,4 +123,26 @@ describe("Data Portability EIP-712 helpers", () => {
{ name: "recordId", type: "bytes32" },
]);
});

it("builds a signed withdrawal authorization without a recipient", async () => {
const account = privateKeyToAccount(
"0x59c6995e998f97a5a0044966f094538e8a55c3611c5a70cfa2de42b44397316c",
);
const typedData = buildWithdrawAuthorizationTypedData(CONFIG, {
account: account.address,
asset: NATIVE_VANA_ASSET,
amount: 42n,
withdrawNonce: 7n,
deadline: 1_800_000_000n,
});

expect(typedData.message).not.toHaveProperty("recipient");
const signature = await account.signTypedData(typedData);
const recovered = await recoverTypedDataAddress({
...typedData,
signature,
});

expect(recovered).toBe(account.address);
});
});
57 changes: 57 additions & 0 deletions packages/vana-sdk/src/protocol/eip712.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,19 @@ export function escrowPaymentDomain(
);
}

/**
* Domain for a gateway-authorized escrow withdrawal.
*
* Withdrawals use the escrow contract as their verifying contract, just like
* generic payments. The distinct primary type prevents a payment signature
* from authorizing a withdrawal.
*/
export function withdrawAuthorizationDomain(
config: DataPortabilityGatewayConfig,
): TypedDataDomain {
return escrowPaymentDomain(config);
}

// grantVersion is a monotonic uint256 nonce per (grantor, grantee) pair. The
// gateway rejects any registration whose version is <= the stored value,
// which is the replay-attack defence now that re-registering the same pair
Expand Down Expand Up @@ -171,6 +184,23 @@ export const GENERIC_PAYMENT_TYPES = {
],
} as const;

/**
* Authorization consumed by `POST /v1/escrow/withdraw`.
*
* The current escrow contract always pays `account` itself. Do not add an
* unsigned recipient field: a future recipient capability must be introduced
* as a new, signed protocol type after the contract and gateway support it.
*/
export const WITHDRAW_AUTHORIZATION_TYPES = {
WithdrawAuthorization: [
{ name: "account", type: "address" },
{ name: "asset", type: "address" },
{ name: "amount", type: "uint256" },
{ name: "withdrawNonce", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
} as const;

// AddData is signed by the data point's owner — registers (scope, dataHash,
// metadataHash) on DataRegistryV2. expectedVersion is the version the caller
// believes is current; the contract rejects with a CAS error if it isn't.
Expand Down Expand Up @@ -240,6 +270,33 @@ export interface GenericPaymentMessage {
paymentNonce: bigint;
}

/** EIP-712 message authorizing a withdrawal of an account's escrow balance. */
export interface WithdrawAuthorizationMessage {
/** The escrow account to debit and the withdrawal recipient. */
account: `0x${string}`;
/** Native VANA sentinel or the ERC-20 asset contract. */
asset: `0x${string}`;
/** Base-unit amount. */
amount: bigint;
/** Caller-managed, strictly increasing nonce for newly accepted intents. */
withdrawNonce: bigint;
/** Unix seconds. Bounds first acceptance; exact accepted retries remain valid. */
deadline: bigint;
}

/** Builds the typed data a wallet must sign before calling the withdraw API. */
export function buildWithdrawAuthorizationTypedData(
config: DataPortabilityGatewayConfig,
message: WithdrawAuthorizationMessage,
) {
return {
domain: withdrawAuthorizationDomain(config),
types: WITHDRAW_AUTHORIZATION_TYPES,
primaryType: "WithdrawAuthorization" as const,
message,
};
}

export interface AddDataMessage {
ownerAddress: `0x${string}`;
scope: string;
Expand Down
Loading
Loading