From 5959b11467c2b10eace2880982db981b63cb500e Mon Sep 17 00:00:00 2001 From: ariessa Date: Thu, 20 Aug 2026 19:02:18 +0800 Subject: [PATCH] chore(solana): resync devnet IDLs and regenerate clients --- .../instructions/claimEscrowRefund.ts | 70 +++++- .../instructions/closeIntent.ts | 200 +++++++++++++++++- .../instructions/initialize.ts | 6 +- .../instructions/initialize.ts | 6 +- .../instructions/cleanupProposedTerms.ts | 77 +++++-- .../instructions/initialize.ts | 6 +- .../types/proposedTermsCleanedUp.ts | 14 ++ .../errors/subscriptionState.ts | 4 + .../instructions/activateSubscription.ts | 98 ++++++++- src/core/solana/idl/fund_transfer_hook.json | 117 +++++++--- src/core/solana/idl/multi_hook_router.json | 58 ++--- src/core/solana/idl/subscription_hook.json | 75 ++++--- src/core/solana/idl/subscription_state.json | 36 +++- src/core/solana/jobStateRetryGuard.ts | 2 +- 14 files changed, 615 insertions(+), 154 deletions(-) diff --git a/src/core/solana/generated/fund-transfer-hook/instructions/claimEscrowRefund.ts b/src/core/solana/generated/fund-transfer-hook/instructions/claimEscrowRefund.ts index a13d54b..5ab54d0 100644 --- a/src/core/solana/generated/fund-transfer-hook/instructions/claimEscrowRefund.ts +++ b/src/core/solana/generated/fund-transfer-hook/instructions/claimEscrowRefund.ts @@ -66,6 +66,8 @@ export type ClaimEscrowRefundInstruction< TAccountEscrowAuthority extends string | AccountMeta = string, TAccountTokenProgram extends string | AccountMeta = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", + TAccountAcpState extends string | AccountMeta = string, + TAccountPlatformTreasury extends string | AccountMeta = string, TRemainingAccounts extends readonly AccountMeta[] = [], > = Instruction & InstructionWithData & @@ -99,6 +101,12 @@ export type ClaimEscrowRefundInstruction< TAccountTokenProgram extends string ? ReadonlyAccount : TAccountTokenProgram, + TAccountAcpState extends string + ? ReadonlyAccount + : TAccountAcpState, + TAccountPlatformTreasury extends string + ? WritableAccount + : TAccountPlatformTreasury, ...TRemainingAccounts, ] >; @@ -147,6 +155,8 @@ export type ClaimEscrowRefundAsyncInput< TAccountProviderTokenAccount extends string = string, TAccountEscrowAuthority extends string = string, TAccountTokenProgram extends string = string, + TAccountAcpState extends string = string, + TAccountPlatformTreasury extends string = string, > = { caller: TransactionSigner; hookState?: Address; @@ -163,6 +173,10 @@ export type ClaimEscrowRefundAsyncInput< providerTokenAccount: Address; escrowAuthority?: Address; tokenProgram?: Address; + /** against `hook_state.acp_program`. Read only to source the treasury. */ + acpState: Address; + /** equal `acp_state.platform_treasury`. */ + platformTreasury: Address; jobId: ClaimEscrowRefundInstructionDataArgs["jobId"]; }; @@ -176,6 +190,8 @@ export async function getClaimEscrowRefundInstructionAsync< TAccountProviderTokenAccount extends string, TAccountEscrowAuthority extends string, TAccountTokenProgram extends string, + TAccountAcpState extends string, + TAccountPlatformTreasury extends string, TProgramAddress extends Address = typeof FUND_TRANSFER_HOOK_PROGRAM_ADDRESS, >( input: ClaimEscrowRefundAsyncInput< @@ -187,7 +203,9 @@ export async function getClaimEscrowRefundInstructionAsync< TAccountEscrowVault, TAccountProviderTokenAccount, TAccountEscrowAuthority, - TAccountTokenProgram + TAccountTokenProgram, + TAccountAcpState, + TAccountPlatformTreasury >, config?: { programAddress?: TProgramAddress }, ): Promise< @@ -201,7 +219,9 @@ export async function getClaimEscrowRefundInstructionAsync< TAccountEscrowVault, TAccountProviderTokenAccount, TAccountEscrowAuthority, - TAccountTokenProgram + TAccountTokenProgram, + TAccountAcpState, + TAccountPlatformTreasury > > { // Program address. @@ -228,6 +248,11 @@ export async function getClaimEscrowRefundInstructionAsync< isWritable: false, }, tokenProgram: { value: input.tokenProgram ?? null, isWritable: false }, + acpState: { value: input.acpState ?? null, isWritable: false }, + platformTreasury: { + value: input.platformTreasury ?? null, + isWritable: true, + }, }; const accounts = originalAccounts as Record< keyof typeof originalAccounts, @@ -268,6 +293,8 @@ export async function getClaimEscrowRefundInstructionAsync< getAccountMeta(accounts.providerTokenAccount), getAccountMeta(accounts.escrowAuthority), getAccountMeta(accounts.tokenProgram), + getAccountMeta(accounts.acpState), + getAccountMeta(accounts.platformTreasury), ], data: getClaimEscrowRefundInstructionDataEncoder().encode( args as ClaimEscrowRefundInstructionDataArgs, @@ -283,7 +310,9 @@ export async function getClaimEscrowRefundInstructionAsync< TAccountEscrowVault, TAccountProviderTokenAccount, TAccountEscrowAuthority, - TAccountTokenProgram + TAccountTokenProgram, + TAccountAcpState, + TAccountPlatformTreasury >); } @@ -297,6 +326,8 @@ export type ClaimEscrowRefundInput< TAccountProviderTokenAccount extends string = string, TAccountEscrowAuthority extends string = string, TAccountTokenProgram extends string = string, + TAccountAcpState extends string = string, + TAccountPlatformTreasury extends string = string, > = { caller: TransactionSigner; hookState: Address; @@ -313,6 +344,10 @@ export type ClaimEscrowRefundInput< providerTokenAccount: Address; escrowAuthority: Address; tokenProgram?: Address; + /** against `hook_state.acp_program`. Read only to source the treasury. */ + acpState: Address; + /** equal `acp_state.platform_treasury`. */ + platformTreasury: Address; jobId: ClaimEscrowRefundInstructionDataArgs["jobId"]; }; @@ -326,6 +361,8 @@ export function getClaimEscrowRefundInstruction< TAccountProviderTokenAccount extends string, TAccountEscrowAuthority extends string, TAccountTokenProgram extends string, + TAccountAcpState extends string, + TAccountPlatformTreasury extends string, TProgramAddress extends Address = typeof FUND_TRANSFER_HOOK_PROGRAM_ADDRESS, >( input: ClaimEscrowRefundInput< @@ -337,7 +374,9 @@ export function getClaimEscrowRefundInstruction< TAccountEscrowVault, TAccountProviderTokenAccount, TAccountEscrowAuthority, - TAccountTokenProgram + TAccountTokenProgram, + TAccountAcpState, + TAccountPlatformTreasury >, config?: { programAddress?: TProgramAddress }, ): ClaimEscrowRefundInstruction< @@ -350,7 +389,9 @@ export function getClaimEscrowRefundInstruction< TAccountEscrowVault, TAccountProviderTokenAccount, TAccountEscrowAuthority, - TAccountTokenProgram + TAccountTokenProgram, + TAccountAcpState, + TAccountPlatformTreasury > { // Program address. const programAddress = @@ -376,6 +417,11 @@ export function getClaimEscrowRefundInstruction< isWritable: false, }, tokenProgram: { value: input.tokenProgram ?? null, isWritable: false }, + acpState: { value: input.acpState ?? null, isWritable: false }, + platformTreasury: { + value: input.platformTreasury ?? null, + isWritable: true, + }, }; const accounts = originalAccounts as Record< keyof typeof originalAccounts, @@ -403,6 +449,8 @@ export function getClaimEscrowRefundInstruction< getAccountMeta(accounts.providerTokenAccount), getAccountMeta(accounts.escrowAuthority), getAccountMeta(accounts.tokenProgram), + getAccountMeta(accounts.acpState), + getAccountMeta(accounts.platformTreasury), ], data: getClaimEscrowRefundInstructionDataEncoder().encode( args as ClaimEscrowRefundInstructionDataArgs, @@ -418,7 +466,9 @@ export function getClaimEscrowRefundInstruction< TAccountEscrowVault, TAccountProviderTokenAccount, TAccountEscrowAuthority, - TAccountTokenProgram + TAccountTokenProgram, + TAccountAcpState, + TAccountPlatformTreasury >); } @@ -443,6 +493,10 @@ export type ParsedClaimEscrowRefundInstruction< providerTokenAccount: TAccountMetas[6]; escrowAuthority: TAccountMetas[7]; tokenProgram: TAccountMetas[8]; + /** against `hook_state.acp_program`. Read only to source the treasury. */ + acpState: TAccountMetas[9]; + /** equal `acp_state.platform_treasury`. */ + platformTreasury: TAccountMetas[10]; }; data: ClaimEscrowRefundInstructionData; }; @@ -455,7 +509,7 @@ export function parseClaimEscrowRefundInstruction< InstructionWithAccounts & InstructionWithData, ): ParsedClaimEscrowRefundInstruction { - if (instruction.accounts.length < 9) { + if (instruction.accounts.length < 11) { // TODO: Coded error. throw new Error("Not enough accounts"); } @@ -477,6 +531,8 @@ export function parseClaimEscrowRefundInstruction< providerTokenAccount: getNextAccount(), escrowAuthority: getNextAccount(), tokenProgram: getNextAccount(), + acpState: getNextAccount(), + platformTreasury: getNextAccount(), }, data: getClaimEscrowRefundInstructionDataDecoder().decode(instruction.data), }; diff --git a/src/core/solana/generated/fund-transfer-hook/instructions/closeIntent.ts b/src/core/solana/generated/fund-transfer-hook/instructions/closeIntent.ts index da313a3..e0966ae 100644 --- a/src/core/solana/generated/fund-transfer-hook/instructions/closeIntent.ts +++ b/src/core/solana/generated/fund-transfer-hook/instructions/closeIntent.ts @@ -32,6 +32,7 @@ import { type WritableAccount, type WritableSignerAccount, } from "@solana/kit"; +import { findHookStatePda } from "../pdas/index.js"; import { FUND_TRANSFER_HOOK_PROGRAM_ADDRESS } from "../programs/index.js"; import { getAccountMetaFactory, type ResolvedAccount } from "../shared/index.js"; @@ -50,8 +51,11 @@ export type CloseIntentInstruction< TAccountActor extends string | AccountMeta = string, TAccountIntent extends string | AccountMeta = string, TAccountFundRequestMap extends string | AccountMeta = string, + TAccountHookState extends string | AccountMeta = string, TAccountSystemProgram extends string | AccountMeta = "11111111111111111111111111111111", + TAccountAcpState extends string | AccountMeta = string, + TAccountPlatformTreasury extends string | AccountMeta = string, TRemainingAccounts extends readonly AccountMeta[] = [], > = Instruction & InstructionWithData & @@ -67,9 +71,18 @@ export type CloseIntentInstruction< TAccountFundRequestMap extends string ? ReadonlyAccount : TAccountFundRequestMap, + TAccountHookState extends string + ? ReadonlyAccount + : TAccountHookState, TAccountSystemProgram extends string ? ReadonlyAccount : TAccountSystemProgram, + TAccountAcpState extends string + ? ReadonlyAccount + : TAccountAcpState, + TAccountPlatformTreasury extends string + ? WritableAccount + : TAccountPlatformTreasury, ...TRemainingAccounts, ] >; @@ -108,15 +121,147 @@ export function getCloseIntentInstructionDataCodec(): FixedSizeCodec< ); } +export type CloseIntentAsyncInput< + TAccountActor extends string = string, + TAccountIntent extends string = string, + TAccountFundRequestMap extends string = string, + TAccountHookState extends string = string, + TAccountSystemProgram extends string = string, + TAccountAcpState extends string = string, + TAccountPlatformTreasury extends string = string, +> = { + /** + * The original actor who proposed the intent (provider for fund requests). + * Still required to authorize the close even though the rent now goes to + * the treasury: the intent PDA is reused in place by the next proposal, so + * letting a stranger deallocate it would force the sponsor to pay its rent + * again for no gain to anyone. + */ + actor: TransactionSigner; + /** + * The intent PDA to close; must be unsigned and non-escrow. The address + * does not encode the intent id, so `intent.id == intent_id` is asserted explicitly. + */ + intent: Address; + /** Per-job fund request intent map; prevents closing the currently active intent. */ + fundRequestMap: Address; + hookState?: Address; + systemProgram?: Address; + /** against `hook_state.acp_program`. Read only to source the treasury. */ + acpState: Address; + /** `acp_state.platform_treasury`. */ + platformTreasury: Address; + intentId: CloseIntentInstructionDataArgs["intentId"]; +}; + +export async function getCloseIntentInstructionAsync< + TAccountActor extends string, + TAccountIntent extends string, + TAccountFundRequestMap extends string, + TAccountHookState extends string, + TAccountSystemProgram extends string, + TAccountAcpState extends string, + TAccountPlatformTreasury extends string, + TProgramAddress extends Address = typeof FUND_TRANSFER_HOOK_PROGRAM_ADDRESS, +>( + input: CloseIntentAsyncInput< + TAccountActor, + TAccountIntent, + TAccountFundRequestMap, + TAccountHookState, + TAccountSystemProgram, + TAccountAcpState, + TAccountPlatformTreasury + >, + config?: { programAddress?: TProgramAddress }, +): Promise< + CloseIntentInstruction< + TProgramAddress, + TAccountActor, + TAccountIntent, + TAccountFundRequestMap, + TAccountHookState, + TAccountSystemProgram, + TAccountAcpState, + TAccountPlatformTreasury + > +> { + // Program address. + const programAddress = + config?.programAddress ?? FUND_TRANSFER_HOOK_PROGRAM_ADDRESS; + + // Original accounts. + const originalAccounts = { + actor: { value: input.actor ?? null, isWritable: true }, + intent: { value: input.intent ?? null, isWritable: true }, + fundRequestMap: { value: input.fundRequestMap ?? null, isWritable: false }, + hookState: { value: input.hookState ?? null, isWritable: false }, + systemProgram: { value: input.systemProgram ?? null, isWritable: false }, + acpState: { value: input.acpState ?? null, isWritable: false }, + platformTreasury: { + value: input.platformTreasury ?? null, + isWritable: true, + }, + }; + const accounts = originalAccounts as Record< + keyof typeof originalAccounts, + ResolvedAccount + >; + + // Original args. + const args = { ...input }; + + // Resolve default values. + if (!accounts.hookState.value) { + accounts.hookState.value = await findHookStatePda(); + } + if (!accounts.systemProgram.value) { + accounts.systemProgram.value = + "11111111111111111111111111111111" as Address<"11111111111111111111111111111111">; + } + + const getAccountMeta = getAccountMetaFactory(programAddress, "programId"); + return Object.freeze({ + accounts: [ + getAccountMeta(accounts.actor), + getAccountMeta(accounts.intent), + getAccountMeta(accounts.fundRequestMap), + getAccountMeta(accounts.hookState), + getAccountMeta(accounts.systemProgram), + getAccountMeta(accounts.acpState), + getAccountMeta(accounts.platformTreasury), + ], + data: getCloseIntentInstructionDataEncoder().encode( + args as CloseIntentInstructionDataArgs, + ), + programAddress, + } as CloseIntentInstruction< + TProgramAddress, + TAccountActor, + TAccountIntent, + TAccountFundRequestMap, + TAccountHookState, + TAccountSystemProgram, + TAccountAcpState, + TAccountPlatformTreasury + >); +} + export type CloseIntentInput< TAccountActor extends string = string, TAccountIntent extends string = string, TAccountFundRequestMap extends string = string, + TAccountHookState extends string = string, TAccountSystemProgram extends string = string, + TAccountAcpState extends string = string, + TAccountPlatformTreasury extends string = string, > = { /** - * The original actor who paid for the intent (provider for fund requests). - * Receives the reclaimed rent lamports. + * The original actor who proposed the intent (provider for fund requests). + * Still required to authorize the close even though the rent now goes to + * the treasury: the intent PDA is reused in place by the next proposal, so + * letting a stranger deallocate it would force the sponsor to pay its rent + * again for no gain to anyone. */ actor: TransactionSigner; /** @@ -126,7 +271,12 @@ export type CloseIntentInput< intent: Address; /** Per-job fund request intent map; prevents closing the currently active intent. */ fundRequestMap: Address; + hookState: Address; systemProgram?: Address; + /** against `hook_state.acp_program`. Read only to source the treasury. */ + acpState: Address; + /** `acp_state.platform_treasury`. */ + platformTreasury: Address; intentId: CloseIntentInstructionDataArgs["intentId"]; }; @@ -134,14 +284,20 @@ export function getCloseIntentInstruction< TAccountActor extends string, TAccountIntent extends string, TAccountFundRequestMap extends string, + TAccountHookState extends string, TAccountSystemProgram extends string, + TAccountAcpState extends string, + TAccountPlatformTreasury extends string, TProgramAddress extends Address = typeof FUND_TRANSFER_HOOK_PROGRAM_ADDRESS, >( input: CloseIntentInput< TAccountActor, TAccountIntent, TAccountFundRequestMap, - TAccountSystemProgram + TAccountHookState, + TAccountSystemProgram, + TAccountAcpState, + TAccountPlatformTreasury >, config?: { programAddress?: TProgramAddress }, ): CloseIntentInstruction< @@ -149,7 +305,10 @@ export function getCloseIntentInstruction< TAccountActor, TAccountIntent, TAccountFundRequestMap, - TAccountSystemProgram + TAccountHookState, + TAccountSystemProgram, + TAccountAcpState, + TAccountPlatformTreasury > { // Program address. const programAddress = @@ -160,7 +319,13 @@ export function getCloseIntentInstruction< actor: { value: input.actor ?? null, isWritable: true }, intent: { value: input.intent ?? null, isWritable: true }, fundRequestMap: { value: input.fundRequestMap ?? null, isWritable: false }, + hookState: { value: input.hookState ?? null, isWritable: false }, systemProgram: { value: input.systemProgram ?? null, isWritable: false }, + acpState: { value: input.acpState ?? null, isWritable: false }, + platformTreasury: { + value: input.platformTreasury ?? null, + isWritable: true, + }, }; const accounts = originalAccounts as Record< keyof typeof originalAccounts, @@ -182,7 +347,10 @@ export function getCloseIntentInstruction< getAccountMeta(accounts.actor), getAccountMeta(accounts.intent), getAccountMeta(accounts.fundRequestMap), + getAccountMeta(accounts.hookState), getAccountMeta(accounts.systemProgram), + getAccountMeta(accounts.acpState), + getAccountMeta(accounts.platformTreasury), ], data: getCloseIntentInstructionDataEncoder().encode( args as CloseIntentInstructionDataArgs, @@ -193,7 +361,10 @@ export function getCloseIntentInstruction< TAccountActor, TAccountIntent, TAccountFundRequestMap, - TAccountSystemProgram + TAccountHookState, + TAccountSystemProgram, + TAccountAcpState, + TAccountPlatformTreasury >); } @@ -204,8 +375,11 @@ export type ParsedCloseIntentInstruction< programAddress: Address; accounts: { /** - * The original actor who paid for the intent (provider for fund requests). - * Receives the reclaimed rent lamports. + * The original actor who proposed the intent (provider for fund requests). + * Still required to authorize the close even though the rent now goes to + * the treasury: the intent PDA is reused in place by the next proposal, so + * letting a stranger deallocate it would force the sponsor to pay its rent + * again for no gain to anyone. */ actor: TAccountMetas[0]; /** @@ -215,7 +389,12 @@ export type ParsedCloseIntentInstruction< intent: TAccountMetas[1]; /** Per-job fund request intent map; prevents closing the currently active intent. */ fundRequestMap: TAccountMetas[2]; - systemProgram: TAccountMetas[3]; + hookState: TAccountMetas[3]; + systemProgram: TAccountMetas[4]; + /** against `hook_state.acp_program`. Read only to source the treasury. */ + acpState: TAccountMetas[5]; + /** `acp_state.platform_treasury`. */ + platformTreasury: TAccountMetas[6]; }; data: CloseIntentInstructionData; }; @@ -228,7 +407,7 @@ export function parseCloseIntentInstruction< InstructionWithAccounts & InstructionWithData, ): ParsedCloseIntentInstruction { - if (instruction.accounts.length < 4) { + if (instruction.accounts.length < 7) { // TODO: Coded error. throw new Error("Not enough accounts"); } @@ -244,7 +423,10 @@ export function parseCloseIntentInstruction< actor: getNextAccount(), intent: getNextAccount(), fundRequestMap: getNextAccount(), + hookState: getNextAccount(), systemProgram: getNextAccount(), + acpState: getNextAccount(), + platformTreasury: getNextAccount(), }, data: getCloseIntentInstructionDataDecoder().decode(instruction.data), }; diff --git a/src/core/solana/generated/fund-transfer-hook/instructions/initialize.ts b/src/core/solana/generated/fund-transfer-hook/instructions/initialize.ts index fc19eef..428d694 100644 --- a/src/core/solana/generated/fund-transfer-hook/instructions/initialize.ts +++ b/src/core/solana/generated/fund-transfer-hook/instructions/initialize.ts @@ -188,9 +188,9 @@ export async function getInitializeInstructionAsync< seeds: [ getBytesEncoder().encode( new Uint8Array([ - 99, 80, 179, 118, 113, 157, 172, 156, 120, 235, 172, 99, 23, 155, - 108, 227, 80, 135, 184, 199, 34, 228, 170, 203, 0, 150, 10, 8, 205, - 195, 225, 17, + 246, 71, 52, 188, 141, 72, 100, 245, 18, 10, 17, 229, 45, 152, 144, + 5, 191, 169, 137, 86, 3, 180, 252, 183, 196, 60, 27, 104, 33, 19, + 36, 51, ]), ), ], diff --git a/src/core/solana/generated/multi-hook-router/instructions/initialize.ts b/src/core/solana/generated/multi-hook-router/instructions/initialize.ts index d66839b..1ddac22 100644 --- a/src/core/solana/generated/multi-hook-router/instructions/initialize.ts +++ b/src/core/solana/generated/multi-hook-router/instructions/initialize.ts @@ -198,9 +198,9 @@ export async function getInitializeInstructionAsync< seeds: [ getBytesEncoder().encode( new Uint8Array([ - 39, 92, 81, 151, 240, 84, 54, 168, 218, 152, 52, 47, 154, 197, 170, - 241, 51, 103, 195, 132, 43, 55, 221, 33, 38, 56, 190, 33, 142, 233, - 87, 232, + 203, 9, 225, 30, 61, 29, 149, 241, 219, 111, 190, 59, 76, 171, 208, + 45, 19, 245, 181, 160, 87, 233, 72, 170, 14, 181, 126, 111, 15, 157, + 236, 230, ]), ), ], diff --git a/src/core/solana/generated/subscription-hook/instructions/cleanupProposedTerms.ts b/src/core/solana/generated/subscription-hook/instructions/cleanupProposedTerms.ts index a3b38c0..d660a06 100644 --- a/src/core/solana/generated/subscription-hook/instructions/cleanupProposedTerms.ts +++ b/src/core/solana/generated/subscription-hook/instructions/cleanupProposedTerms.ts @@ -52,7 +52,8 @@ export type CleanupProposedTermsInstruction< TAccountHookState extends string | AccountMeta = string, TAccountJobAccount extends string | AccountMeta = string, TAccountProposedTerms extends string | AccountMeta = string, - TAccountProvider extends string | AccountMeta = string, + TAccountAcpState extends string | AccountMeta = string, + TAccountPlatformTreasury extends string | AccountMeta = string, TRemainingAccounts extends readonly AccountMeta[] = [], > = Instruction & InstructionWithData & @@ -71,9 +72,12 @@ export type CleanupProposedTermsInstruction< TAccountProposedTerms extends string ? WritableAccount : TAccountProposedTerms, - TAccountProvider extends string - ? WritableAccount - : TAccountProvider, + TAccountAcpState extends string + ? ReadonlyAccount + : TAccountAcpState, + TAccountPlatformTreasury extends string + ? WritableAccount + : TAccountPlatformTreasury, ...TRemainingAccounts, ] >; @@ -122,7 +126,8 @@ export type CleanupProposedTermsAsyncInput< TAccountHookState extends string = string, TAccountJobAccount extends string = string, TAccountProposedTerms extends string = string, - TAccountProvider extends string = string, + TAccountAcpState extends string = string, + TAccountPlatformTreasury extends string = string, > = { /** Permissionless trigger; the only authorization is that the job has expired. */ caller: TransactionSigner; @@ -131,7 +136,9 @@ export type CleanupProposedTermsAsyncInput< jobAccount: Address; /** Manually closed after the provider check. */ proposedTerms: Address; - provider: Address; + /** against `hook_state.acp_program`. Read only to source the treasury. */ + acpState: Address; + platformTreasury: Address; jobId: CleanupProposedTermsInstructionDataArgs["jobId"]; }; @@ -140,7 +147,8 @@ export async function getCleanupProposedTermsInstructionAsync< TAccountHookState extends string, TAccountJobAccount extends string, TAccountProposedTerms extends string, - TAccountProvider extends string, + TAccountAcpState extends string, + TAccountPlatformTreasury extends string, TProgramAddress extends Address = typeof SUBSCRIPTION_HOOK_PROGRAM_ADDRESS, >( input: CleanupProposedTermsAsyncInput< @@ -148,7 +156,8 @@ export async function getCleanupProposedTermsInstructionAsync< TAccountHookState, TAccountJobAccount, TAccountProposedTerms, - TAccountProvider + TAccountAcpState, + TAccountPlatformTreasury >, config?: { programAddress?: TProgramAddress }, ): Promise< @@ -158,7 +167,8 @@ export async function getCleanupProposedTermsInstructionAsync< TAccountHookState, TAccountJobAccount, TAccountProposedTerms, - TAccountProvider + TAccountAcpState, + TAccountPlatformTreasury > > { // Program address. @@ -171,7 +181,11 @@ export async function getCleanupProposedTermsInstructionAsync< hookState: { value: input.hookState ?? null, isWritable: false }, jobAccount: { value: input.jobAccount ?? null, isWritable: false }, proposedTerms: { value: input.proposedTerms ?? null, isWritable: true }, - provider: { value: input.provider ?? null, isWritable: true }, + acpState: { value: input.acpState ?? null, isWritable: false }, + platformTreasury: { + value: input.platformTreasury ?? null, + isWritable: true, + }, }; const accounts = originalAccounts as Record< keyof typeof originalAccounts, @@ -193,7 +207,8 @@ export async function getCleanupProposedTermsInstructionAsync< getAccountMeta(accounts.hookState), getAccountMeta(accounts.jobAccount), getAccountMeta(accounts.proposedTerms), - getAccountMeta(accounts.provider), + getAccountMeta(accounts.acpState), + getAccountMeta(accounts.platformTreasury), ], data: getCleanupProposedTermsInstructionDataEncoder().encode( args as CleanupProposedTermsInstructionDataArgs, @@ -205,7 +220,8 @@ export async function getCleanupProposedTermsInstructionAsync< TAccountHookState, TAccountJobAccount, TAccountProposedTerms, - TAccountProvider + TAccountAcpState, + TAccountPlatformTreasury >); } @@ -214,7 +230,8 @@ export type CleanupProposedTermsInput< TAccountHookState extends string = string, TAccountJobAccount extends string = string, TAccountProposedTerms extends string = string, - TAccountProvider extends string = string, + TAccountAcpState extends string = string, + TAccountPlatformTreasury extends string = string, > = { /** Permissionless trigger; the only authorization is that the job has expired. */ caller: TransactionSigner; @@ -223,7 +240,9 @@ export type CleanupProposedTermsInput< jobAccount: Address; /** Manually closed after the provider check. */ proposedTerms: Address; - provider: Address; + /** against `hook_state.acp_program`. Read only to source the treasury. */ + acpState: Address; + platformTreasury: Address; jobId: CleanupProposedTermsInstructionDataArgs["jobId"]; }; @@ -232,7 +251,8 @@ export function getCleanupProposedTermsInstruction< TAccountHookState extends string, TAccountJobAccount extends string, TAccountProposedTerms extends string, - TAccountProvider extends string, + TAccountAcpState extends string, + TAccountPlatformTreasury extends string, TProgramAddress extends Address = typeof SUBSCRIPTION_HOOK_PROGRAM_ADDRESS, >( input: CleanupProposedTermsInput< @@ -240,7 +260,8 @@ export function getCleanupProposedTermsInstruction< TAccountHookState, TAccountJobAccount, TAccountProposedTerms, - TAccountProvider + TAccountAcpState, + TAccountPlatformTreasury >, config?: { programAddress?: TProgramAddress }, ): CleanupProposedTermsInstruction< @@ -249,7 +270,8 @@ export function getCleanupProposedTermsInstruction< TAccountHookState, TAccountJobAccount, TAccountProposedTerms, - TAccountProvider + TAccountAcpState, + TAccountPlatformTreasury > { // Program address. const programAddress = @@ -261,7 +283,11 @@ export function getCleanupProposedTermsInstruction< hookState: { value: input.hookState ?? null, isWritable: false }, jobAccount: { value: input.jobAccount ?? null, isWritable: false }, proposedTerms: { value: input.proposedTerms ?? null, isWritable: true }, - provider: { value: input.provider ?? null, isWritable: true }, + acpState: { value: input.acpState ?? null, isWritable: false }, + platformTreasury: { + value: input.platformTreasury ?? null, + isWritable: true, + }, }; const accounts = originalAccounts as Record< keyof typeof originalAccounts, @@ -278,7 +304,8 @@ export function getCleanupProposedTermsInstruction< getAccountMeta(accounts.hookState), getAccountMeta(accounts.jobAccount), getAccountMeta(accounts.proposedTerms), - getAccountMeta(accounts.provider), + getAccountMeta(accounts.acpState), + getAccountMeta(accounts.platformTreasury), ], data: getCleanupProposedTermsInstructionDataEncoder().encode( args as CleanupProposedTermsInstructionDataArgs, @@ -290,7 +317,8 @@ export function getCleanupProposedTermsInstruction< TAccountHookState, TAccountJobAccount, TAccountProposedTerms, - TAccountProvider + TAccountAcpState, + TAccountPlatformTreasury >); } @@ -307,7 +335,9 @@ export type ParsedCleanupProposedTermsInstruction< jobAccount: TAccountMetas[2]; /** Manually closed after the provider check. */ proposedTerms: TAccountMetas[3]; - provider: TAccountMetas[4]; + /** against `hook_state.acp_program`. Read only to source the treasury. */ + acpState: TAccountMetas[4]; + platformTreasury: TAccountMetas[5]; }; data: CleanupProposedTermsInstructionData; }; @@ -320,7 +350,7 @@ export function parseCleanupProposedTermsInstruction< InstructionWithAccounts & InstructionWithData, ): ParsedCleanupProposedTermsInstruction { - if (instruction.accounts.length < 5) { + if (instruction.accounts.length < 6) { // TODO: Coded error. throw new Error("Not enough accounts"); } @@ -337,7 +367,8 @@ export function parseCleanupProposedTermsInstruction< hookState: getNextAccount(), jobAccount: getNextAccount(), proposedTerms: getNextAccount(), - provider: getNextAccount(), + acpState: getNextAccount(), + platformTreasury: getNextAccount(), }, data: getCleanupProposedTermsInstructionDataDecoder().decode( instruction.data, diff --git a/src/core/solana/generated/subscription-hook/instructions/initialize.ts b/src/core/solana/generated/subscription-hook/instructions/initialize.ts index 52e21ee..3aaacd9 100644 --- a/src/core/solana/generated/subscription-hook/instructions/initialize.ts +++ b/src/core/solana/generated/subscription-hook/instructions/initialize.ts @@ -195,9 +195,9 @@ export async function getInitializeInstructionAsync< seeds: [ getBytesEncoder().encode( new Uint8Array([ - 20, 0, 166, 80, 93, 113, 128, 167, 240, 179, 33, 165, 80, 28, 221, - 130, 51, 63, 58, 69, 102, 183, 31, 211, 194, 2, 150, 90, 229, 86, - 114, 131, + 82, 35, 22, 92, 78, 34, 36, 77, 223, 4, 88, 68, 137, 148, 42, 102, + 63, 194, 165, 181, 196, 243, 93, 127, 58, 161, 228, 131, 1, 90, 114, + 14, ]), ), ], diff --git a/src/core/solana/generated/subscription-hook/types/proposedTermsCleanedUp.ts b/src/core/solana/generated/subscription-hook/types/proposedTermsCleanedUp.ts index ecd8d91..a7723ce 100644 --- a/src/core/solana/generated/subscription-hook/types/proposedTermsCleanedUp.ts +++ b/src/core/solana/generated/subscription-hook/types/proposedTermsCleanedUp.ts @@ -22,14 +22,26 @@ import { export type ProposedTermsCleanedUp = { jobId: bigint; + /** + * Provider recorded on the terms. Informational only — it is no longer the + * rent destination; see `rent_destination`. + */ provider: Address; lamportsRefunded: bigint; + /** Where the reclaimed rent actually went (the platform treasury). */ + rentDestination: Address; }; export type ProposedTermsCleanedUpArgs = { jobId: number | bigint; + /** + * Provider recorded on the terms. Informational only — it is no longer the + * rent destination; see `rent_destination`. + */ provider: Address; lamportsRefunded: number | bigint; + /** Where the reclaimed rent actually went (the platform treasury). */ + rentDestination: Address; }; export function getProposedTermsCleanedUpEncoder(): FixedSizeEncoder { @@ -37,6 +49,7 @@ export function getProposedTermsCleanedUpEncoder(): FixedSizeEncoder & - AccountSignerMeta + ? WritableAccount : TAccountPayer, TAccountWriterRegistry extends string ? ReadonlyAccount @@ -147,7 +145,28 @@ export type ActivateSubscriptionAsyncInput< TAccountSubscriptionExpiry extends string = string, TAccountSystemProgram extends string = string, > = { - payer: TransactionSigner; + /** + * The job's provider. Named, never debited: this handler does not allocate + * (see below), so nothing is spent here. It stays in the account list + * because the calling hook credits it the `proposed_terms` rent refund + * immediately after this CPI returns. + * + * Deliberately NOT `Signer`. `sub_expiry` is allocated ahead of time by + * `pre_create_sub_expiry`, so by the time activation runs there is nothing + * to fund and therefore nothing for a signature to authorize. Requiring one + * anyway forced the provider to co-sign the evaluator's `complete` — the + * only place in the protocol where two independent parties had to sign a + * single transaction. + * + * Authorization does not rest on this account: the caller binds it to the + * job's provider (subscription-hook `after_action.rs` requires + * `payer_info.key() == terms.provider`), and the provider's consent to + * these terms was already recorded — and signed for — when it funded + * `proposed_terms` at `set_budget`. + * + * refund the hook credits after this CPI returns. + */ + payer: Address; /** * The calling program must be a registered writer. * PDA seeded by the writer's program ID. Writer identity validated in handler. @@ -159,8 +178,13 @@ export type ActivateSubscriptionAsyncInput< */ writerSigner: TransactionSigner; /** - * init_if_needed is safe: the monotonic expiry check prevents backward - * reinitialization and no close path exists for this PDA. + * Must already exist — allocated by `pre_create_sub_expiry`. Untyped here + * only so its absence surfaces as `SubExpiryNotPreCreated` rather than an + * opaque Anchor deserialization failure; the handler does the typed read. + * The monotonic expiry check prevents backward reinitialization and no + * close path exists for this PDA. + * + * deserialize in the handler before any field is trusted. */ subscriptionExpiry?: Address; systemProgram?: Address; @@ -262,7 +286,28 @@ export type ActivateSubscriptionInput< TAccountSubscriptionExpiry extends string = string, TAccountSystemProgram extends string = string, > = { - payer: TransactionSigner; + /** + * The job's provider. Named, never debited: this handler does not allocate + * (see below), so nothing is spent here. It stays in the account list + * because the calling hook credits it the `proposed_terms` rent refund + * immediately after this CPI returns. + * + * Deliberately NOT `Signer`. `sub_expiry` is allocated ahead of time by + * `pre_create_sub_expiry`, so by the time activation runs there is nothing + * to fund and therefore nothing for a signature to authorize. Requiring one + * anyway forced the provider to co-sign the evaluator's `complete` — the + * only place in the protocol where two independent parties had to sign a + * single transaction. + * + * Authorization does not rest on this account: the caller binds it to the + * job's provider (subscription-hook `after_action.rs` requires + * `payer_info.key() == terms.provider`), and the provider's consent to + * these terms was already recorded — and signed for — when it funded + * `proposed_terms` at `set_budget`. + * + * refund the hook credits after this CPI returns. + */ + payer: Address; /** * The calling program must be a registered writer. * PDA seeded by the writer's program ID. Writer identity validated in handler. @@ -274,8 +319,13 @@ export type ActivateSubscriptionInput< */ writerSigner: TransactionSigner; /** - * init_if_needed is safe: the monotonic expiry check prevents backward - * reinitialization and no close path exists for this PDA. + * Must already exist — allocated by `pre_create_sub_expiry`. Untyped here + * only so its absence surfaces as `SubExpiryNotPreCreated` rather than an + * opaque Anchor deserialization failure; the handler does the typed read. + * The monotonic expiry check prevents backward reinitialization and no + * close path exists for this PDA. + * + * deserialize in the handler before any field is trusted. */ subscriptionExpiry: Address; systemProgram?: Address; @@ -367,6 +417,27 @@ export type ParsedActivateSubscriptionInstruction< > = { programAddress: Address; accounts: { + /** + * The job's provider. Named, never debited: this handler does not allocate + * (see below), so nothing is spent here. It stays in the account list + * because the calling hook credits it the `proposed_terms` rent refund + * immediately after this CPI returns. + * + * Deliberately NOT `Signer`. `sub_expiry` is allocated ahead of time by + * `pre_create_sub_expiry`, so by the time activation runs there is nothing + * to fund and therefore nothing for a signature to authorize. Requiring one + * anyway forced the provider to co-sign the evaluator's `complete` — the + * only place in the protocol where two independent parties had to sign a + * single transaction. + * + * Authorization does not rest on this account: the caller binds it to the + * job's provider (subscription-hook `after_action.rs` requires + * `payer_info.key() == terms.provider`), and the provider's consent to + * these terms was already recorded — and signed for — when it funded + * `proposed_terms` at `set_budget`. + * + * refund the hook credits after this CPI returns. + */ payer: TAccountMetas[0]; /** * The calling program must be a registered writer. @@ -379,8 +450,13 @@ export type ParsedActivateSubscriptionInstruction< */ writerSigner: TAccountMetas[2]; /** - * init_if_needed is safe: the monotonic expiry check prevents backward - * reinitialization and no close path exists for this PDA. + * Must already exist — allocated by `pre_create_sub_expiry`. Untyped here + * only so its absence surfaces as `SubExpiryNotPreCreated` rather than an + * opaque Anchor deserialization failure; the handler does the typed read. + * The monotonic expiry check prevents backward reinitialization and no + * close path exists for this PDA. + * + * deserialize in the handler before any field is trusted. */ subscriptionExpiry: TAccountMetas[3]; systemProgram: TAccountMetas[4]; diff --git a/src/core/solana/idl/fund_transfer_hook.json b/src/core/solana/idl/fund_transfer_hook.json index 8109869..ca59bf5 100644 --- a/src/core/solana/idl/fund_transfer_hook.json +++ b/src/core/solana/idl/fund_transfer_hook.json @@ -401,6 +401,19 @@ { "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "name": "token_program" + }, + { + "docs": [ + "against `hook_state.acp_program`. Read only to source the treasury." + ], + "name": "acp_state" + }, + { + "docs": [ + "equal `acp_state.platform_treasury`." + ], + "name": "platform_treasury", + "writable": true } ], "args": [ @@ -428,8 +441,11 @@ "accounts": [ { "docs": [ - "The original actor who paid for the intent (provider for fund requests).", - "Receives the reclaimed rent lamports." + "The original actor who proposed the intent (provider for fund requests).", + "Still required to authorize the close even though the rent now goes to", + "the treasury: the intent PDA is reused in place by the next proposal, so", + "letting a stranger deallocate it would force the sponsor to pay its rent", + "again for no gain to anyone." ], "name": "actor", "signer": true, @@ -485,9 +501,44 @@ ] } }, + { + "name": "hook_state", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 104, + 111, + 111, + 107, + 95, + 115, + 116, + 97, + 116, + 101 + ] + } + ] + } + }, { "address": "11111111111111111111111111111111", "name": "system_program" + }, + { + "docs": [ + "against `hook_state.acp_program`. Read only to source the treasury." + ], + "name": "acp_state" + }, + { + "docs": [ + "`acp_state.platform_treasury`." + ], + "name": "platform_treasury", + "writable": true } ], "args": [ @@ -614,38 +665,38 @@ { "kind": "const", "value": [ - 99, - 80, - 179, - 118, - 113, - 157, - 172, - 156, - 120, - 235, - 172, - 99, - 23, - 155, - 108, - 227, - 80, - 135, - 184, - 199, - 34, - 228, - 170, - 203, - 0, - 150, + 246, + 71, + 52, + 188, + 141, + 72, + 100, + 245, + 18, 10, - 8, - 205, - 195, - 225, - 17 + 17, + 229, + 45, + 152, + 144, + 5, + 191, + 169, + 137, + 86, + 3, + 180, + 252, + 183, + 196, + 60, + 27, + 104, + 33, + 19, + 36, + 51 ] } ] diff --git a/src/core/solana/idl/multi_hook_router.json b/src/core/solana/idl/multi_hook_router.json index 2658f6a..bf293d8 100644 --- a/src/core/solana/idl/multi_hook_router.json +++ b/src/core/solana/idl/multi_hook_router.json @@ -871,38 +871,38 @@ { "kind": "const", "value": [ - 39, - 92, - 81, - 151, - 240, - 84, - 54, - 168, - 218, - 152, - 52, - 47, - 154, - 197, - 170, + 203, + 9, + 225, + 30, + 61, + 29, + 149, 241, - 51, - 103, - 195, - 132, - 43, - 55, - 221, - 33, - 38, - 56, + 219, + 111, 190, - 33, - 142, - 233, + 59, + 76, + 171, + 208, + 45, + 19, + 245, + 181, + 160, 87, - 232 + 233, + 72, + 170, + 14, + 181, + 126, + 111, + 15, + 157, + 236, + 230 ] } ] diff --git a/src/core/solana/idl/subscription_hook.json b/src/core/solana/idl/subscription_hook.json index 0cf0059..006ba0e 100644 --- a/src/core/solana/idl/subscription_hook.json +++ b/src/core/solana/idl/subscription_hook.json @@ -252,7 +252,13 @@ "writable": true }, { - "name": "provider", + "docs": [ + "against `hook_state.acp_program`. Read only to source the treasury." + ], + "name": "acp_state" + }, + { + "name": "platform_treasury", "writable": true } ], @@ -381,38 +387,38 @@ { "kind": "const", "value": [ - 20, - 0, - 166, - 80, - 93, - 113, - 128, - 167, - 240, - 179, - 33, - 165, - 80, - 28, - 221, - 130, - 51, - 63, - 58, - 69, + 82, + 35, + 22, + 92, + 78, + 34, + 36, + 77, + 223, + 4, + 88, + 68, + 137, + 148, + 42, 102, - 183, - 31, - 211, + 63, 194, - 2, - 150, + 165, + 181, + 196, + 243, + 93, + 127, + 58, + 161, + 228, + 131, + 1, 90, - 229, - 86, 114, - 131 + 14 ] } ] @@ -585,12 +591,23 @@ "type": "u64" }, { + "docs": [ + "Provider recorded on the terms. Informational only — it is no longer the", + "rent destination; see `rent_destination`." + ], "name": "provider", "type": "pubkey" }, { "name": "lamports_refunded", "type": "u64" + }, + { + "docs": [ + "Where the reclaimed rent actually went (the platform treasury)." + ], + "name": "rent_destination", + "type": "pubkey" } ], "kind": "struct" diff --git a/src/core/solana/idl/subscription_state.json b/src/core/solana/idl/subscription_state.json index 39fc628..b0189a3 100644 --- a/src/core/solana/idl/subscription_state.json +++ b/src/core/solana/idl/subscription_state.json @@ -65,6 +65,11 @@ "code": 6004, "msg": "Writer has been deactivated", "name": "WriterDeactivated" + }, + { + "code": 6005, + "msg": "sub_expiry must be pre-created; the provider calls pre_create_sub_expiry", + "name": "SubExpiryNotPreCreated" } ], "events": [ @@ -186,8 +191,28 @@ { "accounts": [ { + "docs": [ + "The job's provider. Named, never debited: this handler does not allocate", + "(see below), so nothing is spent here. It stays in the account list", + "because the calling hook credits it the `proposed_terms` rent refund", + "immediately after this CPI returns.", + "", + "Deliberately NOT `Signer`. `sub_expiry` is allocated ahead of time by", + "`pre_create_sub_expiry`, so by the time activation runs there is nothing", + "to fund and therefore nothing for a signature to authorize. Requiring one", + "anyway forced the provider to co-sign the evaluator's `complete` — the", + "only place in the protocol where two independent parties had to sign a", + "single transaction.", + "", + "Authorization does not rest on this account: the caller binds it to the", + "job's provider (subscription-hook `after_action.rs` requires", + "`payer_info.key() == terms.provider`), and the provider's consent to", + "these terms was already recorded — and signed for — when it funded", + "`proposed_terms` at `set_budget`.", + "", + "refund the hook credits after this CPI returns." + ], "name": "payer", - "signer": true, "writable": true }, { @@ -227,8 +252,13 @@ }, { "docs": [ - "init_if_needed is safe: the monotonic expiry check prevents backward", - "reinitialization and no close path exists for this PDA." + "Must already exist — allocated by `pre_create_sub_expiry`. Untyped here", + "only so its absence surfaces as `SubExpiryNotPreCreated` rather than an", + "opaque Anchor deserialization failure; the handler does the typed read.", + "The monotonic expiry check prevents backward reinitialization and no", + "close path exists for this PDA.", + "", + "deserialize in the handler before any field is trusted." ], "name": "subscription_expiry", "pda": { diff --git a/src/core/solana/jobStateRetryGuard.ts b/src/core/solana/jobStateRetryGuard.ts index 5113816..c670d1e 100644 --- a/src/core/solana/jobStateRetryGuard.ts +++ b/src/core/solana/jobStateRetryGuard.ts @@ -136,7 +136,7 @@ const ROUTER_STATE_GATES: StateGate[] = [ // abandoned job's ProposedTerms PDA requires the job to have reached Expired // (cleanup_proposed_terms.rs:47 — state only, no clock check), which happens // on claim_refund. Its job account is at index 2: -// caller, hook_state, job_account, proposed_terms, provider. +// caller, hook_state, job_account, proposed_terms, acp_state, platform_treasury. // No expiry gate: Expired IS the terminal state the instruction wants, so // job.expiredAt being in the past is the precondition, not a disqualifier. const SUB_HOOK_STATE_GATES: StateGate[] = [