diff --git a/packages/sdk-core/src/contracts/sysio/chains/Actions.ts b/packages/sdk-core/src/contracts/sysio/chains/Actions.ts index 102a015..5f52ea3 100644 --- a/packages/sdk-core/src/contracts/sysio/chains/Actions.ts +++ b/packages/sdk-core/src/contracts/sysio/chains/Actions.ts @@ -6,9 +6,11 @@ import { DEFAULT_CHAINS_CONTRACT, MAX_EXTERNAL_CHAIN_ID } from "./Constants.js" import { descriptor } from "./Descriptor.js" import { chainSlugData } from "./Slug.js" import type { + ChainOutpostAddresses, ChainRegistration, CreateActivateChainActionOptions, - CreateRegisterChainActionOptions + CreateRegisterChainActionOptions, + CreateSetOutpostActionOptions } from "./Types.js" function assertExternalChainId(value: number): number { @@ -19,6 +21,23 @@ function assertExternalChainId(value: number): number { return value } +/** + * Fills the generated `outpost_addrs` struct, defaulting every omitted role to + * an empty string. The protocol treats empty as "not configured yet" and both + * operator daemons skip such a chain, so an omitted field is a deferral rather + * than a silent wrong value. + */ +function outpostAddressData( + outpost: Partial | undefined +): SysioContracts.SysioChainsOutpostAddrsType { + return { + opp_addr: outpost?.oppAddress ?? "", + opp_inbound_addr: outpost?.oppInboundAddress ?? "", + operator_registry_addr: outpost?.operatorRegistryAddress ?? "", + source_deposit_addr: outpost?.sourceDepositAddress ?? "" + } +} + /** Creates generated action data for `sysio.chains::regchain`. */ export function createRegisterChainActionData( registration: ChainRegistration @@ -28,7 +47,8 @@ export function createRegisterChainActionData( code: chainSlugData(registration.code), external_chain_id: assertExternalChainId(registration.externalChainId), name: registration.name, - description: registration.description + description: registration.description, + outpost: outpostAddressData(registration.outpost) } } @@ -44,6 +64,26 @@ export function createRegisterChainAction( }) } +/** Creates generated action data for `sysio.chains::setoutpost`. */ +export function createSetOutpostActionData( + code: CreateSetOutpostActionOptions["code"], + outpost: CreateSetOutpostActionOptions["outpost"] +): SysioContracts.SysioChainsSetoutpostAction { + return { code: chainSlugData(code), outpost: outpostAddressData(outpost) } +} + +/** Creates an unsigned privileged `sysio.chains::setoutpost` action. */ +export function createSetOutpostAction( + options: CreateSetOutpostActionOptions +): Action { + return buildContractAction({ + contract: options.contract || DEFAULT_CHAINS_CONTRACT, + descriptor: descriptor.actions.setoutpost, + authorization: options.authorization, + data: createSetOutpostActionData(options.code, options.outpost) + }) +} + /** Creates generated action data for `sysio.chains::activchain`. */ export function createActivateChainActionData( code: CreateActivateChainActionOptions["code"] diff --git a/packages/sdk-core/src/contracts/sysio/chains/Client.ts b/packages/sdk-core/src/contracts/sysio/chains/Client.ts index 4c6f205..167f563 100644 --- a/packages/sdk-core/src/contracts/sysio/chains/Client.ts +++ b/packages/sdk-core/src/contracts/sysio/chains/Client.ts @@ -66,6 +66,12 @@ export function normalizeChainRow( active: row.active, registeredAtMs: BigInt(row.registered_at_ms.toString()), activatedAtMs: BigInt(row.activated_at_ms.toString()), + outpost: { + oppAddress: row.outpost?.opp_addr ?? "", + oppInboundAddress: row.outpost?.opp_inbound_addr ?? "", + operatorRegistryAddress: row.outpost?.operator_registry_addr ?? "", + sourceDepositAddress: row.outpost?.source_deposit_addr ?? "" + }, raw: row } } diff --git a/packages/sdk-core/src/contracts/sysio/chains/Descriptor.ts b/packages/sdk-core/src/contracts/sysio/chains/Descriptor.ts index 5a9b464..9b9c6eb 100644 --- a/packages/sdk-core/src/contracts/sysio/chains/Descriptor.ts +++ b/packages/sdk-core/src/contracts/sysio/chains/Descriptor.ts @@ -2,7 +2,11 @@ import type { ContractDescriptor } from "../../Contract.js" import type * as SysioContracts from "../../../types/SysioContractTypes.js" import { DEFAULT_CHAINS_CONTRACT } from "./Constants.js" -import { ChainsActivateChain, ChainsRegisterChain } from "./Structs.js" +import { + ChainsActivateChain, + ChainsRegisterChain, + ChainsSetOutpost +} from "./Structs.js" /** Generated `sysio.chains` action data keyed by ABI action name. */ export interface SysioChainsActionData { @@ -10,6 +14,8 @@ export interface SysioChainsActionData { regchain: SysioContracts.SysioChainsRegchainAction /** `sysio.chains::activchain` action data. */ activchain: SysioContracts.SysioChainsActivchainAction + /** `sysio.chains::setoutpost` action data. */ + setoutpost: SysioContracts.SysioChainsSetoutpostAction } /** Generated `sysio.chains` table rows keyed by ABI table name. */ @@ -32,6 +38,10 @@ export const descriptor: ContractDescriptor< activchain: { name: "activchain", serialize: data => ChainsActivateChain.from(data) + }, + setoutpost: { + name: "setoutpost", + serialize: data => ChainsSetOutpost.from(data) } }, tables: { diff --git a/packages/sdk-core/src/contracts/sysio/chains/Structs.ts b/packages/sdk-core/src/contracts/sysio/chains/Structs.ts index 343dd5b..ab5b6eb 100644 --- a/packages/sdk-core/src/contracts/sysio/chains/Structs.ts +++ b/packages/sdk-core/src/contracts/sysio/chains/Structs.ts @@ -8,6 +8,29 @@ export class ChainsSlugName extends Struct { @Struct.field("uint64") declare value: UInt64 } +/** + * Runtime serializer for the nested `outpost_addrs` struct. + * + * An EVM outpost names a distinct contract per role; an SVM outpost is one + * program named by `opp_addr`, and `sysio.chains` requires the role fields to be + * empty for it. A chain may also be registered before its remote contracts + * exist, leaving every field empty until `setoutpost` fills them in. + */ +@Struct.type("outpost_addrs") +export class ChainsOutpostAddrs extends Struct { + /** EVM: the OPP contract. SVM: the outpost program id. */ + @Struct.field("string") declare opp_addr: string + + /** EVM: the OPPInbound contract. Empty for SVM. */ + @Struct.field("string") declare opp_inbound_addr: string + + /** EVM: the OperatorRegistry contract. Empty for SVM. */ + @Struct.field("string") declare operator_registry_addr: string + + /** EVM: the source swap-deposit contract. Empty for SVM. */ + @Struct.field("string") declare source_deposit_addr: string +} + /** Runtime serializer for `sysio.chains::regchain`. */ @Struct.type("regchain") export class ChainsRegisterChain extends Struct { @@ -25,6 +48,19 @@ export class ChainsRegisterChain extends Struct { /** Human-readable chain description. */ @Struct.field("string") declare description: string + + /** Remote outpost contract identities for this chain. */ + @Struct.field(ChainsOutpostAddrs) declare outpost: ChainsOutpostAddrs +} + +/** Runtime serializer for `sysio.chains::setoutpost`. */ +@Struct.type("setoutpost") +export class ChainsSetOutpost extends Struct { + /** Stable protocol chain code whose deployment is being replaced. */ + @Struct.field(ChainsSlugName) declare code: ChainsSlugName + + /** Replacement remote contract identities. The whole set is replaced. */ + @Struct.field(ChainsOutpostAddrs) declare outpost: ChainsOutpostAddrs } /** Runtime serializer for `sysio.chains::activchain`. */ diff --git a/packages/sdk-core/src/contracts/sysio/chains/Types.ts b/packages/sdk-core/src/contracts/sysio/chains/Types.ts index 199e7dc..6b29dc2 100644 --- a/packages/sdk-core/src/contracts/sysio/chains/Types.ts +++ b/packages/sdk-core/src/contracts/sysio/chains/Types.ts @@ -14,6 +14,25 @@ export interface ChainsClientOptions { contract?: NameType } +/** + * Remote outpost contract identities for one chain. + * + * An EVM outpost deploys a distinct contract per role. An SVM outpost is a + * single program named by `oppAddress`; the protocol requires the role fields + * to be empty there, and readers fall back to `oppAddress` for every role. Every + * field may be empty while the remote contracts are not deployed yet. + */ +export interface ChainOutpostAddresses { + /** EVM: the OPP contract. SVM: the outpost program id. */ + oppAddress: string + /** EVM: the OPPInbound contract. Must be empty for SVM. */ + oppInboundAddress: string + /** EVM: the OperatorRegistry contract. Must be empty for SVM. */ + operatorRegistryAddress: string + /** EVM: the source swap-deposit contract. Must be empty for SVM. */ + sourceDepositAddress: string +} + /** User-facing chain registration data. */ export interface ChainRegistration { /** VM/signing family used by the registered chain. */ @@ -26,6 +45,11 @@ export interface ChainRegistration { name: string /** Human-readable chain description. */ description: string + /** + * Remote outpost contract identities. Omit to register the chain before its + * remote contracts exist, then supply them later with `setoutpost`. + */ + outpost?: Partial } /** Options for creating an unsigned `regchain` action. */ @@ -38,6 +62,19 @@ export interface CreateRegisterChainActionOptions { contract?: NameType } +/** Options for creating an unsigned `setoutpost` action. */ +export interface CreateSetOutpostActionOptions { + /** Stable protocol chain code whose deployment is being replaced. */ + code: ChainSlugName + /** Replacement contract identities. The whole set is replaced, so a caller + * updating one address must resend the others. */ + outpost: Partial + /** Permission levels authorizing the action. */ + authorization: ContractPermissionLevel[] + /** Chain registry contract account override. */ + contract?: NameType +} + /** Options for creating an unsigned `activchain` action. */ export interface CreateActivateChainActionOptions { /** Stable protocol chain code to activate. */ @@ -82,6 +119,8 @@ export interface ChainRecord { registeredAtMs: bigint /** Activation time in Unix milliseconds, or zero before activation. */ activatedAtMs: bigint + /** Remote outpost contract identities recorded for this chain. */ + outpost: ChainOutpostAddresses /** Original generated registry row. */ raw: SysioContracts.SysioChainsChainRowType } diff --git a/packages/sdk-core/src/types/SysioContractTypes.ts b/packages/sdk-core/src/types/SysioContractTypes.ts index f80a5ea..64fe118 100644 --- a/packages/sdk-core/src/types/SysioContractTypes.ts +++ b/packages/sdk-core/src/types/SysioContractTypes.ts @@ -316,6 +316,15 @@ export interface SysioChainsChainRowType { active: boolean registered_at_ms: number | string activated_at_ms: number | string + outpost: SysioChainsOutpostAddrsType +} + +/** sysio.chains::outpost_addrs (type) */ +export interface SysioChainsOutpostAddrsType { + opp_addr: string + opp_inbound_addr: string + operator_registry_addr: string + source_deposit_addr: string } /** sysio.chains::regchain (action) */ @@ -325,6 +334,13 @@ export interface SysioChainsRegchainAction { external_chain_id: number name: string description: string + outpost: SysioChainsOutpostAddrsType +} + +/** sysio.chains::setoutpost (action) */ +export interface SysioChainsSetoutpostAction { + code: SysioChainsSlugNameType + outpost: SysioChainsOutpostAddrsType } /** sysio.chains::slug_name (type) */ @@ -337,6 +353,7 @@ export interface SysioChainsContract { actions: { activchain: SysioChainsActivchainAction regchain: SysioChainsRegchainAction + setoutpost: SysioChainsSetoutpostAction } tables: { chains: SysioChainsChainRowType @@ -3704,7 +3721,7 @@ export const SysioContractDefinitions: { } = { [SysioContractName.authex]: { name: SysioContractName.authex, account: "sysio.authex", actions: ["clearlinks", "createlink", "recordlink"], tables: ["links"] }, [SysioContractName.bios]: { name: SysioContractName.bios, account: "sysio", actions: ["activate", "deleteauth", "linkauth", "newaccount", "reqactivated", "reqauth", "setabi", "setalimits", "setcode", "setfinalizer", "setparams", "setpriv", "setprodkeys", "setprods", "unlinkauth", "updateauth"], tables: ["abihash"] }, - [SysioContractName.chains]: { name: SysioContractName.chains, account: "sysio.chains", actions: ["activchain", "regchain"], tables: ["chains"] }, + [SysioContractName.chains]: { name: SysioContractName.chains, account: "sysio.chains", actions: ["activchain", "regchain", "setoutpost"], tables: ["chains"] }, [SysioContractName.chalg]: { name: SysioContractName.chalg, account: "sysio.chalg", actions: ["chkdispute", "chkuwchal", "claimbond", "opendispute", "openuwchal", "slashop", "uwchalbond", "votedispute", "voteuwchal"], tables: ["bondcredits", "chalgstate", "disputes", "disputevote", "uwchals", "uwchalvote"] }, [SysioContractName.councl]: { name: SysioContractName.councl, account: "sysio.councl", actions: ["addcandidate", "finalizeinit", "forceassign", "forceback", "loadtier", "purge", "repcandidate", "reset", "rmcandidate", "settle", "startinit", "stir", "vote"], tables: ["candidates", "config", "council", "roster", "state", "tier2", "tier3", "tier3remap"] }, [SysioContractName.dclaim]: { name: SysioContractName.dclaim, account: "sysio.dclaim", actions: ["claim", "flushexpired", "importdone", "importseed", "linkswept", "onreward", "setclmwindow", "setconfig"], tables: ["capcfg", "capcounters", "pclaims", "rwdcursors", "unmapped"] }, diff --git a/packages/sdk-core/tests/contracts/sysio/chains/Actions.test.ts b/packages/sdk-core/tests/contracts/sysio/chains/Actions.test.ts index 338ff64..e98b634 100644 --- a/packages/sdk-core/tests/contracts/sysio/chains/Actions.test.ts +++ b/packages/sdk-core/tests/contracts/sysio/chains/Actions.test.ts @@ -4,12 +4,27 @@ import { SysioChainsChainkind } from "@wireio/sdk-core/types/SysioContractTypes" const { ChainsActivateChain, ChainsRegisterChain, + ChainsSetOutpost, createActivateChainAction, createActivateChainActionData, createRegisterChainAction, - createRegisterChainActionData + createRegisterChainActionData, + createSetOutpostAction, + createSetOutpostActionData } = contracts.sysio.chains +const EVM_OPP = "0x5FbDB2315678afecb367f032d93F642f64180aa3", + EVM_INBOUND = "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", + EVM_OPREG = "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + EVM_DEPOSIT = "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", + SVM_PROGRAM = "So11111111111111111111111111111111111111112", + EMPTY_OUTPOST = { + opp_addr: "", + opp_inbound_addr: "", + operator_registry_addr: "", + source_deposit_addr: "" + } + const REGISTRATION = { kind: SysioChainsChainkind.CHAIN_KIND_EVM, code: "POLYGON", @@ -25,7 +40,10 @@ describe("sysio.chains action helpers", () => { code: { value: SlugName.from("POLYGON") }, external_chain_id: 137, name: "Polygon", - description: "Polygon EVM outpost" + description: "Polygon EVM outpost", + // Registering before the remote contracts exist is legal; setoutpost + // fills them in later, and both operator daemons skip the chain meanwhile. + outpost: EMPTY_OUTPOST }) expect(createActivateChainActionData("POLYGON")).toEqual({ code: { value: SlugName.from("POLYGON") } @@ -52,9 +70,62 @@ describe("sysio.chains action helpers", () => { expect(Number(registerData.external_chain_id)).toBe(137) expect(registerData.name).toBe("Polygon") expect(registerData.description).toBe("Polygon EVM outpost") + expect(registerData.outpost.opp_addr).toBe("") expect(Number(activateData.code.value)).toBe(SlugName.from("POLYGON")) }) + test("carries every EVM contract role through regchain", () => { + expect( + createRegisterChainActionData({ + ...REGISTRATION, + outpost: { + oppAddress: EVM_OPP, + oppInboundAddress: EVM_INBOUND, + operatorRegistryAddress: EVM_OPREG, + sourceDepositAddress: EVM_DEPOSIT + } + }).outpost + ).toEqual({ + opp_addr: EVM_OPP, + opp_inbound_addr: EVM_INBOUND, + operator_registry_addr: EVM_OPREG, + source_deposit_addr: EVM_DEPOSIT + }) + }) + + test("defaults an omitted role to empty rather than dropping the field", () => { + // An SVM outpost is one program: only opp_addr is set, and the protocol + // REQUIRES the other three to be empty. + expect( + createSetOutpostActionData("SOLANA", { oppAddress: SVM_PROGRAM }) + ).toEqual({ + code: { value: SlugName.from("SOLANA") }, + outpost: { ...EMPTY_OUTPOST, opp_addr: SVM_PROGRAM } + }) + }) + + test("serializes setoutpost through the shared contract proxy", () => { + const action = createSetOutpostAction({ + code: "POLYGON", + outpost: { + oppAddress: EVM_OPP, + oppInboundAddress: EVM_INBOUND, + operatorRegistryAddress: EVM_OPREG, + sourceDepositAddress: EVM_DEPOSIT + }, + authorization: ["sysio.chains@active"] + }), + data = action.decodeData(ChainsSetOutpost) + + expect(action.account.toString()).toBe("sysio.chains") + expect(action.name.toString()).toBe("setoutpost") + expect(Number(data.code.value)).toBe(SlugName.from("POLYGON")) + expect(data.outpost.opp_addr).toBe(EVM_OPP) + expect(data.outpost.opp_inbound_addr).toBe(EVM_INBOUND) + expect(data.outpost.operator_registry_addr).toBe(EVM_OPREG) + expect(data.outpost.source_deposit_addr).toBe(EVM_DEPOSIT) + }) + test("rejects external chain identifiers outside uint32", () => { expect(() => createRegisterChainActionData({