Skip to content
Open
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
44 changes: 42 additions & 2 deletions packages/sdk-core/src/contracts/sysio/chains/Actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<ChainOutpostAddresses> | 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
Expand All @@ -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)
}
}

Expand All @@ -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"]
Expand Down
6 changes: 6 additions & 0 deletions packages/sdk-core/src/contracts/sysio/chains/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
12 changes: 11 additions & 1 deletion packages/sdk-core/src/contracts/sysio/chains/Descriptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@ 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 {
/** `sysio.chains::regchain` action data. */
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. */
Expand All @@ -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: {
Expand Down
36 changes: 36 additions & 0 deletions packages/sdk-core/src/contracts/sysio/chains/Structs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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`. */
Expand Down
39 changes: 39 additions & 0 deletions packages/sdk-core/src/contracts/sysio/chains/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -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<ChainOutpostAddresses>
}

/** Options for creating an unsigned `regchain` action. */
Expand All @@ -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<ChainOutpostAddresses>
/** 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. */
Expand Down Expand Up @@ -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
}
19 changes: 18 additions & 1 deletion packages/sdk-core/src/types/SysioContractTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) */
Expand All @@ -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) */
Expand All @@ -337,6 +353,7 @@ export interface SysioChainsContract {
actions: {
activchain: SysioChainsActivchainAction
regchain: SysioChainsRegchainAction
setoutpost: SysioChainsSetoutpostAction
}
tables: {
chains: SysioChainsChainRowType
Expand Down Expand Up @@ -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"] },
Expand Down
75 changes: 73 additions & 2 deletions packages/sdk-core/tests/contracts/sysio/chains/Actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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") }
Expand All @@ -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({
Expand Down
Loading