From 7235f7a1204e98b518d0b29ab20d6c4ea95b4095 Mon Sep 17 00:00:00 2001 From: joshglogau Date: Tue, 18 Aug 2026 14:58:15 -0400 Subject: [PATCH 1/3] feat(sdk-outpost): add Ethereum node owners --- .../ethereum/EthereumNodeOwnerClient.ts | 254 ++++++++++++++++++ .../clients/ethereum/EthereumOutpostClient.ts | 15 ++ .../sdk-outpost/src/clients/ethereum/Types.ts | 3 + .../sdk-outpost/src/clients/ethereum/index.ts | 1 + .../sdk-outpost/src/deployments/Schema.ts | 3 +- packages/sdk-outpost/src/deployments/Types.ts | 1 + packages/sdk-outpost/tests/Fixtures.ts | 2 +- .../ethereum/EthereumNodeOwnerClient.test.ts | 192 +++++++++++++ .../tests/deployments/Schema.test.ts | 9 + 9 files changed, 478 insertions(+), 2 deletions(-) create mode 100644 packages/sdk-outpost/src/clients/ethereum/EthereumNodeOwnerClient.ts create mode 100644 packages/sdk-outpost/tests/clients/ethereum/EthereumNodeOwnerClient.test.ts diff --git a/packages/sdk-outpost/src/clients/ethereum/EthereumNodeOwnerClient.ts b/packages/sdk-outpost/src/clients/ethereum/EthereumNodeOwnerClient.ts new file mode 100644 index 0000000..39f009c --- /dev/null +++ b/packages/sdk-outpost/src/clients/ethereum/EthereumNodeOwnerClient.ts @@ -0,0 +1,254 @@ +import { KeyType } from "@wireio/sdk-core/chain/KeyType" +import type { Name } from "@wireio/sdk-core/chain/Name" +import type { PublicKey } from "@wireio/sdk-core/chain/PublicKey" +import { + Signer, + constants as ethersConstants, + utils as ethersUtils, + type BigNumberish, + type BytesLike, + type Event, + type providers +} from "ethers" +import { match } from "ts-pattern" + +import { IERC1155__factory, type BAR } from "../../contracts/ethereum/index.js" +import type { WireKeyStruct } from "../../contracts/ethereum/generated/BAR.js" +import type { NodeCommittedEvent } from "../../contracts/ethereum/generated/BAR.js" + +const ConfirmationCount = 1, + NodeCommittedEventName = "NodeCommitted", + UncompressedPublicKeyByteLength = 65, + UncompressedPublicKeyPrefix = 4 + +/** BAR WireKey numeric variants accepted for node-owner account authority. */ +enum NodeOwnerWireKeyType { + K1 = 1, + R1 = 2, + EM = 4, + ED = 5 +} + +/** WireNodes ERC-1155 token ids accepted by BAR as node-owner tiers. */ +export enum EthereumNodeOwnerTier { + T1 = 1, + T2 = 2, + T3 = 3 +} + +const DefaultNodeOwnerTiers = [ + EthereumNodeOwnerTier.T1, + EthereumNodeOwnerTier.T2, + EthereumNodeOwnerTier.T3 +] as const + +/** One owned WireNodes tier returned from the canonical ERC-1155 contract. */ +export interface EthereumNodeOwnerSlotBalance { + /** WireNodes token id and node-owner tier. */ + tokenId: EthereumNodeOwnerTier + /** Number of units held by the queried owner. */ + balance: bigint + /** Canonical WireNodes contract configured by BAR. */ + tokenContractAddress: string +} + +/** Inputs required to escrow a WireNodes unit and register its owner. */ +export interface EthereumNodeOwnerCommitRequest { + /** WireNodes token id and node-owner tier to commit. */ + tokenId: EthereumNodeOwnerTier + /** Canonical Wire account name to create or register. */ + wireAccountName: Name + /** Wire account owner/active authority. */ + wirePublicKey: PublicKey + /** Uncompressed SEC1 secp256k1 public key belonging to the EVM signer. */ + depositorPublicKey: BytesLike +} + +/** Canonical `NodeCommitted` fields emitted by BAR. */ +export interface EthereumNodeCommittedEvent { + /** EVM owner that committed the token. */ + owner: string + /** WireNodes token id and node-owner tier. */ + tokenId: EthereumNodeOwnerTier + /** Canonical WireNodes contract from which BAR pulled custody. */ + tokenContractAddress: string + /** Wire account submitted for registration. */ + wireAccountName: string +} + +/** Confirmed node-owner registration submission. */ +export interface EthereumNodeOwnerCommitSubmission { + /** BAR commit transaction hash. */ + transactionId: string + /** ERC-1155 approval transaction hash when approval was required. */ + approvalTransactionId?: string + /** Confirmed BAR event proving the submitted registration. */ + committed: EthereumNodeCommittedEvent +} + +/** Node-owner slot reads, approval, and registration for one verified outpost. */ +export class EthereumNodeOwnerClient { + /** Bind node-owner operations to a generated BAR contract. */ + constructor( + private readonly bar: BAR, + private readonly connection: providers.Provider | Signer + ) {} + + /** Resolve the governance-configured canonical WireNodes contract. */ + async canonicalTokenContractAddress(): Promise { + const address = ethersUtils.getAddress(await this.bar.wireNodesContract()) + if (address === ethersConstants.AddressZero) { + throw new Error("BAR has no canonical WireNodes contract configured.") + } + return address + } + + /** Return non-zero WireNodes balances for the requested owner and tiers. */ + async ownedSlots( + owner: string, + tokenIds: readonly EthereumNodeOwnerTier[] = DefaultNodeOwnerTiers + ): Promise { + const normalizedOwner = ethersUtils.getAddress(owner), + tokenContractAddress = await this.canonicalTokenContractAddress(), + token = IERC1155__factory.connect(tokenContractAddress, this.connection), + balances = await Promise.all( + tokenIds.map(async tokenId => ({ + tokenId, + balance: (await token.balanceOf(normalizedOwner, tokenId)).toBigInt(), + tokenContractAddress + })) + ) + + return balances.filter(({ balance }) => balance > 0n) + } + + /** Approve BAR when needed, commit one WireNodes unit, and return its event. */ + async commit( + request: EthereumNodeOwnerCommitRequest + ): Promise { + const signer = this.assertSigner(), + owner = ethersUtils.getAddress(await signer.getAddress()), + depositorPublicKey = this.assertDepositorPublicKey( + request.depositorPublicKey, + owner + ), + wireAccountName = this.canonicalAccountName( + request.wireAccountName.toString() + ), + tokenContractAddress = await this.canonicalTokenContractAddress(), + token = IERC1155__factory.connect(tokenContractAddress, signer), + approved = await token.isApprovedForAll(owner, this.bar.address) + + let approvalTransactionId: string | undefined + if (!approved) { + const approval = await token.setApprovalForAll(this.bar.address, true) + approvalTransactionId = approval.hash + await approval.wait(ConfirmationCount) + } + + const transaction = await this.bar.commitNode( + request.tokenId, + wireAccountName, + this.wireKey(request.wirePublicKey), + depositorPublicKey + ), + receipt = await transaction.wait(ConfirmationCount) + + return { + transactionId: transaction.hash, + approvalTransactionId, + committed: EthereumNodeOwnerClient.committedEvent(receipt.events) + } + } + + /** Normalize the canonical `NodeCommitted` event from a BAR receipt. */ + static committedEvent( + events: readonly Event[] | undefined + ): EthereumNodeCommittedEvent { + const event = events?.find( + ({ event: name }) => name === NodeCommittedEventName + ), + committedEvent = event as NodeCommittedEvent | undefined, + { owner, tokenId, nftAddress, wireAccountName } = + committedEvent?.args ?? {} + + if ( + owner == null || + tokenId == null || + nftAddress == null || + wireAccountName == null + ) { + throw new Error("Confirmed BAR transaction did not emit NodeCommitted.") + } + return { + owner: ethersUtils.getAddress(owner), + tokenId: EthereumNodeOwnerClient.nodeOwnerTier(tokenId), + tokenContractAddress: ethersUtils.getAddress(nftAddress), + wireAccountName + } + } + + /** Require a connected EVM signer for node-owner writes. */ + private assertSigner(): Signer { + if (!Signer.isSigner(this.connection)) { + throw new Error("Ethereum node-owner commit requires a connected signer.") + } + return this.connection + } + + /** Validate the depositor key shape and its relationship to the signer. */ + private assertDepositorPublicKey( + value: BytesLike, + owner: string + ): Uint8Array { + const publicKey = ethersUtils.arrayify(value) + if ( + publicKey.length !== UncompressedPublicKeyByteLength || + publicKey[0] !== UncompressedPublicKeyPrefix + ) { + throw new Error( + "depositorPublicKey must be a 65-byte uncompressed SEC1 key." + ) + } + const derivedOwner = ethersUtils.getAddress( + ethersUtils.computeAddress(publicKey) + ) + if (derivedOwner !== owner) { + throw new Error("depositorPublicKey does not belong to the EVM signer.") + } + return publicKey + } + + /** Validate and canonicalize the non-empty Wire account name. */ + private canonicalAccountName(value: string): string { + if (value.length === 0) { + throw new Error("wireAccountName must not be empty.") + } + return value + } + + /** Convert an sdk-core public key into BAR's generated WireKey structure. */ + private wireKey(publicKey: PublicKey): WireKeyStruct { + const keyType = match(publicKey.type) + .with(KeyType.K1, () => NodeOwnerWireKeyType.K1) + .with(KeyType.R1, () => NodeOwnerWireKeyType.R1) + .with(KeyType.EM, () => NodeOwnerWireKeyType.EM) + .with(KeyType.ED, () => NodeOwnerWireKeyType.ED) + .otherwise(type => { + throw new Error(`${type} is not a node-owner authority key type.`) + }) + + return { keyType, key: publicKey.data.array } + } + + /** Normalize one emitted token id to BAR's supported node-owner tier. */ + private static nodeOwnerTier(value: BigNumberish): EthereumNodeOwnerTier { + return match(Number(value.toString())) + .with(EthereumNodeOwnerTier.T1, () => EthereumNodeOwnerTier.T1) + .with(EthereumNodeOwnerTier.T2, () => EthereumNodeOwnerTier.T2) + .with(EthereumNodeOwnerTier.T3, () => EthereumNodeOwnerTier.T3) + .otherwise(tokenId => { + throw new Error(`NodeCommitted emitted unsupported tier ${tokenId}.`) + }) + } +} diff --git a/packages/sdk-outpost/src/clients/ethereum/EthereumOutpostClient.ts b/packages/sdk-outpost/src/clients/ethereum/EthereumOutpostClient.ts index 7586c52..9d02c29 100644 --- a/packages/sdk-outpost/src/clients/ethereum/EthereumOutpostClient.ts +++ b/packages/sdk-outpost/src/clients/ethereum/EthereumOutpostClient.ts @@ -1,4 +1,5 @@ import { + BAR__factory, OPPInbound__factory, OPP__factory, OperatorRegistry__factory, @@ -16,6 +17,7 @@ import { ethereumProvider } from "./Connection.js" import { EthereumContractMap, EthereumOutpostClientOptions } from "./Types.js" import { EthereumReserveClient } from "./EthereumReserveClient.js" import { EthereumReserveSwapClient } from "./EthereumReserveSwapClient.js" +import { EthereumNodeOwnerClient } from "./EthereumNodeOwnerClient.js" /** Strictly typed access to one verified Ethereum outpost deployment. */ export class EthereumOutpostClient { @@ -47,6 +49,10 @@ export class EthereumOutpostClient { this.contract(EthereumContractName.ReserveManager), options.connection ) + this.nodeOwners = new EthereumNodeOwnerClient( + this.contract(EthereumContractName.BAR), + options.connection + ) } /** Reserve creation, cancellation, and reads for this verified outpost. */ @@ -55,6 +61,9 @@ export class EthereumOutpostClient { /** Reserve-swap writes and balance reads for this verified outpost. */ readonly swaps: EthereumReserveSwapClient + /** Node-owner slot reads, approvals, and BAR registration. */ + readonly nodeOwners: EthereumNodeOwnerClient + /** Deployment profile used to verify and connect this client. */ get profile(): EthereumOutpostClientOptions["profile"] { return this.options.profile @@ -64,6 +73,12 @@ export class EthereumOutpostClient { contract(name: T): EthereumContractMap[T] { const { connection, profile } = this.options, contract = match(name as EthereumContractName) + .with(EthereumContractName.BAR, () => + BAR__factory.connect( + profile.ethereum.contracts[EthereumContractName.BAR].address, + connection + ) + ) .with(EthereumContractName.OPP, () => OPP__factory.connect( profile.ethereum.contracts[EthereumContractName.OPP].address, diff --git a/packages/sdk-outpost/src/clients/ethereum/Types.ts b/packages/sdk-outpost/src/clients/ethereum/Types.ts index 2c29ef5..001d764 100644 --- a/packages/sdk-outpost/src/clients/ethereum/Types.ts +++ b/packages/sdk-outpost/src/clients/ethereum/Types.ts @@ -1,4 +1,5 @@ import type { + BAR, OPP, OPPInbound, OperatorRegistry, @@ -19,6 +20,8 @@ export interface EthereumOutpostClientOptions { /** Generated contract clients keyed by their deployment identity. */ export interface EthereumContractMap { + /** Bond and node-owner registration contract. */ + [EthereumContractName.BAR]: BAR /** Outbound OPP endpoint. */ [EthereumContractName.OPP]: OPP /** Inbound OPP endpoint. */ diff --git a/packages/sdk-outpost/src/clients/ethereum/index.ts b/packages/sdk-outpost/src/clients/ethereum/index.ts index c3df2d2..7f8f194 100644 --- a/packages/sdk-outpost/src/clients/ethereum/index.ts +++ b/packages/sdk-outpost/src/clients/ethereum/index.ts @@ -1,3 +1,4 @@ export * from "./EthereumReserveSwapClient.js" export * from "./EthereumReserveClient.js" +export * from "./EthereumNodeOwnerClient.js" export * from "./Types.js" diff --git a/packages/sdk-outpost/src/deployments/Schema.ts b/packages/sdk-outpost/src/deployments/Schema.ts index f7260db..0078328 100644 --- a/packages/sdk-outpost/src/deployments/Schema.ts +++ b/packages/sdk-outpost/src/deployments/Schema.ts @@ -50,7 +50,7 @@ export const SolanaProgramDeploymentProfileSchema = z.object({ /** Immutable compatibility profile for one Wire outpost deployment. */ export const OutpostDeploymentProfileSchema = z .object({ - schemaVersion: z.literal(1), + schemaVersion: z.literal(2), id: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/), deploymentChecksum: Sha256Schema, wire: z.object({ @@ -59,6 +59,7 @@ export const OutpostDeploymentProfileSchema = z ethereum: z.object({ chainId: z.number().int().positive(), contracts: z.object({ + [EthereumContractName.BAR]: EthereumContractDeploymentProfileSchema, [EthereumContractName.OPP]: EthereumContractDeploymentProfileSchema, [EthereumContractName.OPPInbound]: EthereumContractDeploymentProfileSchema, diff --git a/packages/sdk-outpost/src/deployments/Types.ts b/packages/sdk-outpost/src/deployments/Types.ts index fe6a4ef..33abb78 100644 --- a/packages/sdk-outpost/src/deployments/Types.ts +++ b/packages/sdk-outpost/src/deployments/Types.ts @@ -6,6 +6,7 @@ export enum OutpostChainFamily { /** Ethereum contracts owned by the current outpost deployment. */ export enum EthereumContractName { + BAR = "BAR", OPP = "OPP", OPPInbound = "OPPInbound", OperatorRegistry = "OperatorRegistry", diff --git a/packages/sdk-outpost/tests/Fixtures.ts b/packages/sdk-outpost/tests/Fixtures.ts index 94e78ab..aeb7d9b 100644 --- a/packages/sdk-outpost/tests/Fixtures.ts +++ b/packages/sdk-outpost/tests/Fixtures.ts @@ -119,7 +119,7 @@ export function createOutpostDeploymentProfileFixture(): OutpostDeploymentProfil ), programData = createSolanaProgramDataAccountData(), profile = { - schemaVersion: 1, + schemaVersion: 2, id: `${TestWireChainId}-${TestHash.slice(0, 12)}`, deploymentChecksum: TestHash, wire: { chainId: TestWireChainId }, diff --git a/packages/sdk-outpost/tests/clients/ethereum/EthereumNodeOwnerClient.test.ts b/packages/sdk-outpost/tests/clients/ethereum/EthereumNodeOwnerClient.test.ts new file mode 100644 index 0000000..a62d0b6 --- /dev/null +++ b/packages/sdk-outpost/tests/clients/ethereum/EthereumNodeOwnerClient.test.ts @@ -0,0 +1,192 @@ +import { KeyType } from "@wireio/sdk-core/chain/KeyType" +import type { Name } from "@wireio/sdk-core/chain/Name" +import type { PublicKey } from "@wireio/sdk-core/chain/PublicKey" +import { + BigNumber, + Wallet, + constants as ethersConstants, + providers, + utils as ethersUtils, + type Event +} from "ethers" + +import { + EthereumNodeOwnerClient, + EthereumNodeOwnerTier, + IERC1155__factory, + type BAR, + type IERC1155 +} from "@wireio/sdk-outpost" + +const BarAddress = "0x18E317A7D70d8fBf8e6E893616b52390EbBdb629", + TokenContractAddress = "0x5c74c94173F05dA1720953407cbb920F3DF9f887", + CommitTransactionHash = `0x${"11".repeat(32)}`, + ApprovalTransactionHash = `0x${"22".repeat(32)}`, + TestPrivateKey = `0x${"33".repeat(32)}`, + WireAccountName = "nodeowner", + WireAccount = { + toString: () => WireAccountName + } as Name, + TransactionReceipt = { + blockNumber: 10, + logs: [], + status: 1 + } as unknown as providers.TransactionReceipt + +/** Create a confirmed transaction fixture with an optional parsed event. */ +function transactionFixture( + hash: string, + events: readonly Event[] = [] +): providers.TransactionResponse { + return { + hash, + wait: jest.fn(async () => ({ ...TransactionReceipt, events })) + } as unknown as providers.TransactionResponse +} + +/** Create generated BAR and IERC-1155 fixtures for one node-owner flow. */ +function contractFixtures(approved = true) { + const wallet = new Wallet(TestPrivateKey), + committedEvent = { + event: "NodeCommitted", + args: { + owner: wallet.address, + tokenId: BigNumber.from(EthereumNodeOwnerTier.T2), + nftAddress: TokenContractAddress, + wireAccountName: WireAccountName + } + }, + commitTransaction = transactionFixture(CommitTransactionHash, [ + committedEvent as never + ]), + approvalTransaction = transactionFixture(ApprovalTransactionHash), + bar = { + address: BarAddress, + wireNodesContract: jest.fn(async () => TokenContractAddress), + commitNode: jest.fn(async () => commitTransaction) + } as unknown as BAR, + token = { + balanceOf: jest.fn(async (_owner: string, tokenId: number) => + BigNumber.from(tokenId === EthereumNodeOwnerTier.T2 ? 1 : 0) + ), + isApprovedForAll: jest.fn(async () => approved), + setApprovalForAll: jest.fn(async () => approvalTransaction) + } as unknown as IERC1155 + + jest.spyOn(IERC1155__factory, "connect").mockReturnValue(token) + return { wallet, bar, token, commitTransaction, approvalTransaction } +} + +/** Return an sdk-core public-key input aligned with the EVM test signer. */ +function wirePublicKey(wallet: Wallet): PublicKey { + return { + type: KeyType.K1, + data: { + array: ethersUtils.arrayify(wallet._signingKey().compressedPublicKey) + } + } as PublicKey +} + +afterEach(() => jest.restoreAllMocks()) + +describe("EthereumNodeOwnerClient", () => { + it("reads only owned tiers from BAR's canonical WireNodes contract", async () => { + const { wallet, bar, token } = contractFixtures(), + client = new EthereumNodeOwnerClient(bar, wallet) + + await expect(client.ownedSlots(wallet.address)).resolves.toEqual([ + { + tokenId: EthereumNodeOwnerTier.T2, + balance: 1n, + tokenContractAddress: TokenContractAddress + } + ]) + expect(token.balanceOf).toHaveBeenCalledTimes(3) + }) + + it("commits through BAR without an unnecessary approval", async () => { + const { wallet, bar, token, commitTransaction } = contractFixtures(), + client = new EthereumNodeOwnerClient(bar, wallet), + submission = await client.commit({ + tokenId: EthereumNodeOwnerTier.T2, + wireAccountName: WireAccount, + wirePublicKey: wirePublicKey(wallet), + depositorPublicKey: wallet._signingKey().publicKey + }) + + expect(submission).toEqual({ + transactionId: CommitTransactionHash, + approvalTransactionId: undefined, + committed: { + owner: wallet.address, + tokenId: EthereumNodeOwnerTier.T2, + tokenContractAddress: TokenContractAddress, + wireAccountName: WireAccountName + } + }) + expect(token.setApprovalForAll).not.toHaveBeenCalled() + expect(bar.commitNode).toHaveBeenCalledWith( + EthereumNodeOwnerTier.T2, + WireAccountName, + expect.objectContaining({ keyType: 1 }), + ethersUtils.arrayify(wallet._signingKey().publicKey) + ) + expect(commitTransaction.wait).toHaveBeenCalledWith(1) + }) + + it("confirms ERC-1155 approval before committing when required", async () => { + const { wallet, bar, token, approvalTransaction } = contractFixtures(false), + client = new EthereumNodeOwnerClient(bar, wallet) + + await expect( + client.commit({ + tokenId: EthereumNodeOwnerTier.T2, + wireAccountName: WireAccount, + wirePublicKey: wirePublicKey(wallet), + depositorPublicKey: wallet._signingKey().publicKey + }) + ).resolves.toEqual( + expect.objectContaining({ + approvalTransactionId: ApprovalTransactionHash + }) + ) + expect(token.setApprovalForAll).toHaveBeenCalledWith(BarAddress, true) + expect(approvalTransaction.wait).toHaveBeenCalledWith(1) + }) + + it("rejects provider-only writes and depositor keys from another signer", async () => { + const { wallet, bar } = contractFixtures(), + providerClient = new EthereumNodeOwnerClient( + bar, + new providers.JsonRpcProvider() + ), + signerClient = new EthereumNodeOwnerClient(bar, wallet), + otherWallet = Wallet.createRandom(), + request = { + tokenId: EthereumNodeOwnerTier.T2, + wireAccountName: WireAccount, + wirePublicKey: wirePublicKey(wallet), + depositorPublicKey: wallet._signingKey().publicKey + } + + await expect(providerClient.commit(request)).rejects.toThrow( + "requires a connected signer" + ) + await expect( + signerClient.commit({ + ...request, + depositorPublicKey: otherWallet._signingKey().publicKey + }) + ).rejects.toThrow("does not belong to the EVM signer") + expect(bar.commitNode).not.toHaveBeenCalled() + }) + + it("fails closed when BAR has no canonical WireNodes contract", async () => { + const { wallet, bar } = contractFixtures() + bar.wireNodesContract = jest.fn(async () => ethersConstants.AddressZero) + + await expect( + new EthereumNodeOwnerClient(bar, wallet).canonicalTokenContractAddress() + ).rejects.toThrow("no canonical WireNodes contract") + }) +}) diff --git a/packages/sdk-outpost/tests/deployments/Schema.test.ts b/packages/sdk-outpost/tests/deployments/Schema.test.ts index 6148060..676b532 100644 --- a/packages/sdk-outpost/tests/deployments/Schema.test.ts +++ b/packages/sdk-outpost/tests/deployments/Schema.test.ts @@ -31,6 +31,15 @@ describe("OutpostDeploymentProfileSchema", () => { ) }) + it("rejects a pre-BAR deployment profile schema", () => { + const profile = { + ...createOutpostDeploymentProfileFixture(), + schemaVersion: 1 + } + + expect(() => parseOutpostDeploymentProfile(profile)).toThrow("expected 2") + }) + it("rejects an invalid Solana ProgramData address", () => { const fixture = createOutpostDeploymentProfileFixture() fixture.solana.programs.liqsolCore.programDataAddress = From 6cf288c34ca6d5683b4c93fa6466b5557440e66f Mon Sep 17 00:00:00 2001 From: joshglogau Date: Tue, 18 Aug 2026 15:16:51 -0400 Subject: [PATCH 2/3] fix(sdk-outpost): keep BAR capability optional --- CLAUDE.md | 2 +- packages/sdk-outpost/README.md | 27 +++++++++++++ .../src/artifacts/Compatibility.ts | 4 +- .../clients/ethereum/EthereumOutpostClient.ts | 38 +++++++++++++------ .../sdk-outpost/src/deployments/Schema.ts | 5 ++- .../verification/OutpostDeploymentVerifier.ts | 5 ++- packages/sdk-outpost/tests/Fixtures.ts | 8 ++-- .../ethereum/EthereumOutpostClient.test.ts | 17 +++++++++ .../tests/deployments/Schema.test.ts | 14 ++++--- 9 files changed, 92 insertions(+), 28 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index fca5df6..dc1f662 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -226,7 +226,7 @@ All generated or modified code **must** include JSDoc comments (`/** ... */`), c - `packages/sdk-core/src/contracts/sysio/uwrit` preserves raw request bytes and exposes `sourceRequestId` for swap correlation. External outpost ids are big-endian; synthetic WIRE queue ids are little-endian and high-bit tagged. - `packages/sdk-outpost` owns typed Ethereum/Solana clients and validates caller-supplied immutable deployment profiles. Canonical ABIs, IDLs, runtime templates, program binaries, ethers v6 factories, and Anchor types come directly from exact npm package versions owned by `wire-ethereum` and `wire-solana`; never regenerate them here or replace them with committed sibling links. - `OutpostClient.create` is sdk-outpost's only published client-construction facade. It delegates family selection to an internal factory; concrete Ethereum/Solana instance types remain available for typing, but their backend factories and module paths are not package entrypoints. -- `packages/sdk-outpost` owns external reserve lifecycle and swap execution. Staking remains outside this package until its dedicated migration. +- `packages/sdk-outpost` owns external reserve lifecycle, swap execution, and BAR-backed Ethereum node-owner registration. BAR is an optional deployment capability: profiles without it preserve reserve and swap behavior, while node-owner access fails closed. Node-owner registration must use BAR's canonical WireNodes address and is not staking; staking remains outside this package until its dedicated migration. - `sdk-outpost` accepts caller-owned providers and deployment profiles, verifies exact Ethereum implementations and Solana ProgramData against source-owned runtime artifacts, and never owns mutable endpoint catalogs. A same-code cluster respin requires a new profile, not an artifact or SDK release; any deployable binary change requires both a producer artifact and SDK release. - A connected outpost client proves deployment compatibility, not swap or stake readiness. Wire-chain orchestration remains in `sdk-core`, and consumers must retain flow-specific capability gates. - Publish `sdk-outpost` only through the repository release workflow after its normal build and tests pass. diff --git a/packages/sdk-outpost/README.md b/packages/sdk-outpost/README.md index 5b0e5d3..c449a31 100644 --- a/packages/sdk-outpost/README.md +++ b/packages/sdk-outpost/README.md @@ -142,6 +142,33 @@ Ethereum also exposes `requestErc20WithApproval`, `nativeBalance`, and `erc20Balance`. Solana exposes `requestNative`, `requestSpl`, `nativeBalance`, and `splBalance` through the same `client.swaps` ownership boundary. +## Ethereum node owners + +The verified Ethereum client exposes `nodeOwners` for the external half of the +node-owner flow. It resolves the canonical WireNodes ERC-1155 contract from +BAR, reads owned tiers, obtains approval only when needed, and submits +`BAR.commitNode`. Wire account authority parsing reuses `@wireio/sdk-core`; the +SDK validates that the uncompressed depositor key belongs to the EVM signer. +BAR is an optional deployment capability: schema-v1 profiles without a BAR +identity continue to support the existing reserve and swap clients, while +accessing `nodeOwners` fails closed with an explicit availability error. + +```ts +const slots = await ethereum.nodeOwners.ownedSlots(ownerAddress) +const submission = await ethereum.nodeOwners.commit({ + tokenId: slots[0].tokenId, + wireAccountName: Name.from(wireAccountName), + wirePublicKey: PublicKey.from(wirePublicKey), + depositorPublicKey +}) +``` + +This surface does not mint test tokens, guess a fallback contract, create the +Wire account directly, or infer protocol completion from the EVM receipt. Hub +must keep the action disabled unless deployment and capability evidence both +advertise the complete node-owner flow, then follow the resulting Wire-side +registration state separately. + ## Reserve lifecycle Wallet-connected clients expose the external half of the post-bootstrap diff --git a/packages/sdk-outpost/src/artifacts/Compatibility.ts b/packages/sdk-outpost/src/artifacts/Compatibility.ts index 156395d..b398c1e 100644 --- a/packages/sdk-outpost/src/artifacts/Compatibility.ts +++ b/packages/sdk-outpost/src/artifacts/Compatibility.ts @@ -121,8 +121,10 @@ export function assertOutpostArtifactCompatibility( match(family) .with(OutpostChainFamily.ethereum, () => { Object.values(EthereumContractName).forEach(contractName => { + const contract = profile.ethereum.contracts[contractName] + if (contract == null) return assertInterfaceDigest( - profile.ethereum.contracts[contractName].abiSha256, + contract.abiSha256, OutpostArtifactManifests.ethereum.contracts[contractName].abiSha256, `Ethereum ${contractName} ABI` ) diff --git a/packages/sdk-outpost/src/clients/ethereum/EthereumOutpostClient.ts b/packages/sdk-outpost/src/clients/ethereum/EthereumOutpostClient.ts index 9d02c29..9c5ed80 100644 --- a/packages/sdk-outpost/src/clients/ethereum/EthereumOutpostClient.ts +++ b/packages/sdk-outpost/src/clients/ethereum/EthereumOutpostClient.ts @@ -49,10 +49,12 @@ export class EthereumOutpostClient { this.contract(EthereumContractName.ReserveManager), options.connection ) - this.nodeOwners = new EthereumNodeOwnerClient( - this.contract(EthereumContractName.BAR), - options.connection - ) + if (options.profile.ethereum.contracts[EthereumContractName.BAR] != null) { + this.nodeOwnerClient = new EthereumNodeOwnerClient( + this.contract(EthereumContractName.BAR), + options.connection + ) + } } /** Reserve creation, cancellation, and reads for this verified outpost. */ @@ -61,8 +63,17 @@ export class EthereumOutpostClient { /** Reserve-swap writes and balance reads for this verified outpost. */ readonly swaps: EthereumReserveSwapClient - /** Node-owner slot reads, approvals, and BAR registration. */ - readonly nodeOwners: EthereumNodeOwnerClient + private readonly nodeOwnerClient?: EthereumNodeOwnerClient + + /** Node-owner slot reads, approvals, and BAR registration when deployed. */ + get nodeOwners(): EthereumNodeOwnerClient { + if (this.nodeOwnerClient == null) { + throw new Error( + "Ethereum node owners are unavailable because this deployment profile has no BAR identity." + ) + } + return this.nodeOwnerClient + } /** Deployment profile used to verify and connect this client. */ get profile(): EthereumOutpostClientOptions["profile"] { @@ -73,12 +84,15 @@ export class EthereumOutpostClient { contract(name: T): EthereumContractMap[T] { const { connection, profile } = this.options, contract = match(name as EthereumContractName) - .with(EthereumContractName.BAR, () => - BAR__factory.connect( - profile.ethereum.contracts[EthereumContractName.BAR].address, - connection - ) - ) + .with(EthereumContractName.BAR, () => { + const bar = profile.ethereum.contracts[EthereumContractName.BAR] + if (bar == null) { + throw new Error( + "Ethereum node owners are unavailable because this deployment profile has no BAR identity." + ) + } + return BAR__factory.connect(bar.address, connection) + }) .with(EthereumContractName.OPP, () => OPP__factory.connect( profile.ethereum.contracts[EthereumContractName.OPP].address, diff --git a/packages/sdk-outpost/src/deployments/Schema.ts b/packages/sdk-outpost/src/deployments/Schema.ts index 0078328..9a50a12 100644 --- a/packages/sdk-outpost/src/deployments/Schema.ts +++ b/packages/sdk-outpost/src/deployments/Schema.ts @@ -50,7 +50,7 @@ export const SolanaProgramDeploymentProfileSchema = z.object({ /** Immutable compatibility profile for one Wire outpost deployment. */ export const OutpostDeploymentProfileSchema = z .object({ - schemaVersion: z.literal(2), + schemaVersion: z.literal(1), id: z.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/), deploymentChecksum: Sha256Schema, wire: z.object({ @@ -59,7 +59,8 @@ export const OutpostDeploymentProfileSchema = z ethereum: z.object({ chainId: z.number().int().positive(), contracts: z.object({ - [EthereumContractName.BAR]: EthereumContractDeploymentProfileSchema, + [EthereumContractName.BAR]: + EthereumContractDeploymentProfileSchema.optional(), [EthereumContractName.OPP]: EthereumContractDeploymentProfileSchema, [EthereumContractName.OPPInbound]: EthereumContractDeploymentProfileSchema, diff --git a/packages/sdk-outpost/src/verification/OutpostDeploymentVerifier.ts b/packages/sdk-outpost/src/verification/OutpostDeploymentVerifier.ts index 43a7791..aefd2fb 100644 --- a/packages/sdk-outpost/src/verification/OutpostDeploymentVerifier.ts +++ b/packages/sdk-outpost/src/verification/OutpostDeploymentVerifier.ts @@ -77,8 +77,9 @@ async function verifyEthereum( await Promise.all( Object.values(EthereumContractName).map(async contractName => { - const contract = profile.ethereum.contracts[contractName], - proxyCode = await provider.getCode(contract.address) + const contract = profile.ethereum.contracts[contractName] + if (contract == null) return + const proxyCode = await provider.getCode(contract.address) if (proxyCode === EmptyEthereumCode) { throw new Error( diff --git a/packages/sdk-outpost/tests/Fixtures.ts b/packages/sdk-outpost/tests/Fixtures.ts index aeb7d9b..9e593dc 100644 --- a/packages/sdk-outpost/tests/Fixtures.ts +++ b/packages/sdk-outpost/tests/Fixtures.ts @@ -119,7 +119,7 @@ export function createOutpostDeploymentProfileFixture(): OutpostDeploymentProfil ), programData = createSolanaProgramDataAccountData(), profile = { - schemaVersion: 2, + schemaVersion: 1, id: `${TestWireChainId}-${TestHash.slice(0, 12)}`, deploymentChecksum: TestHash, wire: { chainId: TestWireChainId }, @@ -159,7 +159,7 @@ export function createEthereumProviderFixture( ) jest.spyOn(provider, "getCode").mockImplementation(async address => { const implementation = Object.entries(profile.ethereum.contracts).find( - ([, deployment]) => deployment.implementationAddress === address + ([, deployment]) => deployment?.implementationAddress === address ) if (implementation != null) { return createEthereumImplementationCode( @@ -167,14 +167,14 @@ export function createEthereumProviderFixture( ) } return Object.values(profile.ethereum.contracts).some( - deployment => deployment.address === address + deployment => deployment?.address === address ) ? TestEthereumProxyCode : "0x" }) jest.spyOn(provider, "getStorage").mockImplementation(async address => { const contract = Object.values(profile.ethereum.contracts).find( - deployment => deployment.address === address + deployment => deployment?.address === address ) return zeroPadValue( contract?.implementationAddress ?? diff --git a/packages/sdk-outpost/tests/clients/ethereum/EthereumOutpostClient.test.ts b/packages/sdk-outpost/tests/clients/ethereum/EthereumOutpostClient.test.ts index 74d027d..7ee3a83 100644 --- a/packages/sdk-outpost/tests/clients/ethereum/EthereumOutpostClient.test.ts +++ b/packages/sdk-outpost/tests/clients/ethereum/EthereumOutpostClient.test.ts @@ -123,6 +123,23 @@ describe("EthereumOutpostClient", () => { ) }) + it("keeps existing Ethereum clients usable when BAR is not deployed", async () => { + const profile = createOutpostDeploymentProfileFixture() + delete profile.ethereum.contracts[EthereumContractName.BAR] + const provider = createEthereumProviderFixture(profile), + client = await createEthereumClient({ + profile, + connection: provider + }) + + expect(client.reserves).toBeInstanceOf(EthereumReserveClient) + expect(client.swaps).toBeInstanceOf(EthereumReserveSwapClient) + expect(() => client.nodeOwners).toThrow("has no BAR identity") + expect(provider.getCode).toHaveBeenCalledTimes( + (Object.values(EthereumContractName).length - 1) * 2 + ) + }) + it("parses the protocol deposit id from a confirmed receipt", () => { const events = [ { eventName: "SwapDeposit", args: [42n] } as unknown as EventLog diff --git a/packages/sdk-outpost/tests/deployments/Schema.test.ts b/packages/sdk-outpost/tests/deployments/Schema.test.ts index 676b532..868ea69 100644 --- a/packages/sdk-outpost/tests/deployments/Schema.test.ts +++ b/packages/sdk-outpost/tests/deployments/Schema.test.ts @@ -31,13 +31,15 @@ describe("OutpostDeploymentProfileSchema", () => { ) }) - it("rejects a pre-BAR deployment profile schema", () => { - const profile = { - ...createOutpostDeploymentProfileFixture(), - schemaVersion: 1 - } + it("preserves schema-v1 profiles when BAR is not deployed", () => { + const profile = createOutpostDeploymentProfileFixture() + delete profile.ethereum.contracts[EthereumContractName.BAR] - expect(() => parseOutpostDeploymentProfile(profile)).toThrow("expected 2") + expect( + parseOutpostDeploymentProfile(profile).ethereum.contracts[ + EthereumContractName.BAR + ] + ).toBeUndefined() }) it("rejects an invalid Solana ProgramData address", () => { From f2cafe2488602d0d6391628fa91517ad4c00136b Mon Sep 17 00:00:00 2001 From: joshglogau Date: Fri, 21 Aug 2026 11:50:29 -0400 Subject: [PATCH 3/3] refactor(sdk-outpost): align node owners with ethers v6 --- packages/sdk-outpost/package.json | 1 + .../ethereum/EthereumNodeOwnerClient.ts | 78 +++++++-------- .../ethereum/EthereumNodeOwnerClient.test.ts | 94 +++++++++---------- pnpm-lock.yaml | 3 + 4 files changed, 89 insertions(+), 87 deletions(-) diff --git a/packages/sdk-outpost/package.json b/packages/sdk-outpost/package.json index 16666e1..5e69cac 100644 --- a/packages/sdk-outpost/package.json +++ b/packages/sdk-outpost/package.json @@ -46,6 +46,7 @@ "@solana/web3.js": "^1.98.4", "@wireio/outpost-ethereum-artifacts": "0.2.2", "@wireio/outpost-solana-artifacts": "0.2.1", + "@wireio/sdk-core": "workspace:*", "ethers": "^6.15.0", "ts-pattern": "^5.9.0", "zod": "^4.4.3" diff --git a/packages/sdk-outpost/src/clients/ethereum/EthereumNodeOwnerClient.ts b/packages/sdk-outpost/src/clients/ethereum/EthereumNodeOwnerClient.ts index 39f009c..eaeaf19 100644 --- a/packages/sdk-outpost/src/clients/ethereum/EthereumNodeOwnerClient.ts +++ b/packages/sdk-outpost/src/clients/ethereum/EthereumNodeOwnerClient.ts @@ -1,20 +1,23 @@ -import { KeyType } from "@wireio/sdk-core/chain/KeyType" -import type { Name } from "@wireio/sdk-core/chain/Name" -import type { PublicKey } from "@wireio/sdk-core/chain/PublicKey" +import type { BAR } from "@wireio/outpost-ethereum-artifacts" +import { IERC1155__factory } from "@wireio/outpost-ethereum-artifacts" +import { KeyType, type Name, type PublicKey } from "@wireio/sdk-core" import { - Signer, - constants as ethersConstants, - utils as ethersUtils, + ZeroAddress, + computeAddress, + getAddress, + getBigInt, + getBytes, + hexlify, type BigNumberish, type BytesLike, - type Event, - type providers + type EventLog, + type Log, + type Provider, + type Signer } from "ethers" import { match } from "ts-pattern" -import { IERC1155__factory, type BAR } from "../../contracts/ethereum/index.js" -import type { WireKeyStruct } from "../../contracts/ethereum/generated/BAR.js" -import type { NodeCommittedEvent } from "../../contracts/ethereum/generated/BAR.js" +import { assertEthereumSigner } from "./Connection.js" const ConfirmationCount = 1, NodeCommittedEventName = "NodeCommitted", @@ -91,13 +94,13 @@ export class EthereumNodeOwnerClient { /** Bind node-owner operations to a generated BAR contract. */ constructor( private readonly bar: BAR, - private readonly connection: providers.Provider | Signer + private readonly connection: Provider | Signer ) {} /** Resolve the governance-configured canonical WireNodes contract. */ async canonicalTokenContractAddress(): Promise { - const address = ethersUtils.getAddress(await this.bar.wireNodesContract()) - if (address === ethersConstants.AddressZero) { + const address = getAddress(await this.bar.wireNodesContract()) + if (address === ZeroAddress) { throw new Error("BAR has no canonical WireNodes contract configured.") } return address @@ -108,13 +111,13 @@ export class EthereumNodeOwnerClient { owner: string, tokenIds: readonly EthereumNodeOwnerTier[] = DefaultNodeOwnerTiers ): Promise { - const normalizedOwner = ethersUtils.getAddress(owner), + const normalizedOwner = getAddress(owner), tokenContractAddress = await this.canonicalTokenContractAddress(), token = IERC1155__factory.connect(tokenContractAddress, this.connection), balances = await Promise.all( tokenIds.map(async tokenId => ({ tokenId, - balance: (await token.balanceOf(normalizedOwner, tokenId)).toBigInt(), + balance: getBigInt(await token.balanceOf(normalizedOwner, tokenId)), tokenContractAddress })) ) @@ -127,7 +130,7 @@ export class EthereumNodeOwnerClient { request: EthereumNodeOwnerCommitRequest ): Promise { const signer = this.assertSigner(), - owner = ethersUtils.getAddress(await signer.getAddress()), + owner = getAddress(await signer.getAddress()), depositorPublicKey = this.assertDepositorPublicKey( request.depositorPublicKey, owner @@ -137,11 +140,12 @@ export class EthereumNodeOwnerClient { ), tokenContractAddress = await this.canonicalTokenContractAddress(), token = IERC1155__factory.connect(tokenContractAddress, signer), - approved = await token.isApprovedForAll(owner, this.bar.address) + barAddress = await this.bar.getAddress(), + approved = await token.isApprovedForAll(owner, barAddress) let approvalTransactionId: string | undefined if (!approved) { - const approval = await token.setApprovalForAll(this.bar.address, true) + const approval = await token.setApprovalForAll(barAddress, true) approvalTransactionId = approval.hash await approval.wait(ConfirmationCount) } @@ -157,43 +161,44 @@ export class EthereumNodeOwnerClient { return { transactionId: transaction.hash, approvalTransactionId, - committed: EthereumNodeOwnerClient.committedEvent(receipt.events) + committed: EthereumNodeOwnerClient.committedEvent(receipt?.logs) } } /** Normalize the canonical `NodeCommitted` event from a BAR receipt. */ static committedEvent( - events: readonly Event[] | undefined + events: readonly (EventLog | Log)[] | undefined ): EthereumNodeCommittedEvent { const event = events?.find( - ({ event: name }) => name === NodeCommittedEventName + candidate => + "eventName" in candidate && + candidate.eventName === NodeCommittedEventName ), - committedEvent = event as NodeCommittedEvent | undefined, - { owner, tokenId, nftAddress, wireAccountName } = - committedEvent?.args ?? {} + arguments_ = event != null && "args" in event ? event.args : undefined, + owner = arguments_?.[0], + tokenId = arguments_?.[1], + tokenContractAddress = arguments_?.[2], + wireAccountName = arguments_?.[3] if ( owner == null || tokenId == null || - nftAddress == null || + tokenContractAddress == null || wireAccountName == null ) { throw new Error("Confirmed BAR transaction did not emit NodeCommitted.") } return { - owner: ethersUtils.getAddress(owner), + owner: getAddress(owner), tokenId: EthereumNodeOwnerClient.nodeOwnerTier(tokenId), - tokenContractAddress: ethersUtils.getAddress(nftAddress), + tokenContractAddress: getAddress(tokenContractAddress), wireAccountName } } /** Require a connected EVM signer for node-owner writes. */ private assertSigner(): Signer { - if (!Signer.isSigner(this.connection)) { - throw new Error("Ethereum node-owner commit requires a connected signer.") - } - return this.connection + return assertEthereumSigner(this.connection, "Ethereum node-owner commit") } /** Validate the depositor key shape and its relationship to the signer. */ @@ -201,7 +206,7 @@ export class EthereumNodeOwnerClient { value: BytesLike, owner: string ): Uint8Array { - const publicKey = ethersUtils.arrayify(value) + const publicKey = getBytes(value) if ( publicKey.length !== UncompressedPublicKeyByteLength || publicKey[0] !== UncompressedPublicKeyPrefix @@ -210,10 +215,7 @@ export class EthereumNodeOwnerClient { "depositorPublicKey must be a 65-byte uncompressed SEC1 key." ) } - const derivedOwner = ethersUtils.getAddress( - ethersUtils.computeAddress(publicKey) - ) - if (derivedOwner !== owner) { + if (getAddress(computeAddress(hexlify(publicKey))) !== owner) { throw new Error("depositorPublicKey does not belong to the EVM signer.") } return publicKey @@ -228,7 +230,7 @@ export class EthereumNodeOwnerClient { } /** Convert an sdk-core public key into BAR's generated WireKey structure. */ - private wireKey(publicKey: PublicKey): WireKeyStruct { + private wireKey(publicKey: PublicKey) { const keyType = match(publicKey.type) .with(KeyType.K1, () => NodeOwnerWireKeyType.K1) .with(KeyType.R1, () => NodeOwnerWireKeyType.R1) diff --git a/packages/sdk-outpost/tests/clients/ethereum/EthereumNodeOwnerClient.test.ts b/packages/sdk-outpost/tests/clients/ethereum/EthereumNodeOwnerClient.test.ts index a62d0b6..7fa806c 100644 --- a/packages/sdk-outpost/tests/clients/ethereum/EthereumNodeOwnerClient.test.ts +++ b/packages/sdk-outpost/tests/clients/ethereum/EthereumNodeOwnerClient.test.ts @@ -1,21 +1,19 @@ -import { KeyType } from "@wireio/sdk-core/chain/KeyType" -import type { Name } from "@wireio/sdk-core/chain/Name" -import type { PublicKey } from "@wireio/sdk-core/chain/PublicKey" +import type { BAR, IERC1155 } from "@wireio/outpost-ethereum-artifacts" +import { IERC1155__factory } from "@wireio/outpost-ethereum-artifacts" +import { KeyType, Name, PublicKey } from "@wireio/sdk-core" import { - BigNumber, + JsonRpcProvider, Wallet, - constants as ethersConstants, - providers, - utils as ethersUtils, - type Event + ZeroAddress, + getBytes, + type EventLog, + type TransactionReceipt, + type TransactionResponse } from "ethers" import { EthereumNodeOwnerClient, - EthereumNodeOwnerTier, - IERC1155__factory, - type BAR, - type IERC1155 + EthereumNodeOwnerTier } from "@wireio/sdk-outpost" const BarAddress = "0x18E317A7D70d8fBf8e6E893616b52390EbBdb629", @@ -24,50 +22,51 @@ const BarAddress = "0x18E317A7D70d8fBf8e6E893616b52390EbBdb629", ApprovalTransactionHash = `0x${"22".repeat(32)}`, TestPrivateKey = `0x${"33".repeat(32)}`, WireAccountName = "nodeowner", - WireAccount = { - toString: () => WireAccountName - } as Name, - TransactionReceipt = { - blockNumber: 10, + WireAccount = Name.from(WireAccountName), + TransactionReceiptFixture = { logs: [], status: 1 - } as unknown as providers.TransactionReceipt + } as unknown as TransactionReceipt -/** Create a confirmed transaction fixture with an optional parsed event. */ +/** Create a confirmed transaction fixture with optional parsed logs. */ function transactionFixture( hash: string, - events: readonly Event[] = [] -): providers.TransactionResponse { + logs: readonly EventLog[] = [] +): TransactionResponse { return { hash, - wait: jest.fn(async () => ({ ...TransactionReceipt, events })) - } as unknown as providers.TransactionResponse + wait: jest.fn(async () => ({ ...TransactionReceiptFixture, logs })) + } as unknown as TransactionResponse } /** Create generated BAR and IERC-1155 fixtures for one node-owner flow. */ -function contractFixtures(approved = true) { +function contractFixtures( + approved = true, + tokenContractAddress = TokenContractAddress +) { const wallet = new Wallet(TestPrivateKey), committedEvent = { - event: "NodeCommitted", - args: { - owner: wallet.address, - tokenId: BigNumber.from(EthereumNodeOwnerTier.T2), - nftAddress: TokenContractAddress, - wireAccountName: WireAccountName - } - }, + eventName: "NodeCommitted", + args: [ + wallet.address, + BigInt(EthereumNodeOwnerTier.T2), + TokenContractAddress, + WireAccountName + ] + } as unknown as EventLog, commitTransaction = transactionFixture(CommitTransactionHash, [ - committedEvent as never + committedEvent ]), approvalTransaction = transactionFixture(ApprovalTransactionHash), bar = { - address: BarAddress, - wireNodesContract: jest.fn(async () => TokenContractAddress), + target: BarAddress, + getAddress: jest.fn(async () => BarAddress), + wireNodesContract: jest.fn(async () => tokenContractAddress), commitNode: jest.fn(async () => commitTransaction) } as unknown as BAR, token = { balanceOf: jest.fn(async (_owner: string, tokenId: number) => - BigNumber.from(tokenId === EthereumNodeOwnerTier.T2 ? 1 : 0) + BigInt(tokenId === EthereumNodeOwnerTier.T2 ? 1 : 0) ), isApprovedForAll: jest.fn(async () => approved), setApprovalForAll: jest.fn(async () => approvalTransaction) @@ -79,12 +78,10 @@ function contractFixtures(approved = true) { /** Return an sdk-core public-key input aligned with the EVM test signer. */ function wirePublicKey(wallet: Wallet): PublicKey { - return { + return PublicKey.from({ type: KeyType.K1, - data: { - array: ethersUtils.arrayify(wallet._signingKey().compressedPublicKey) - } - } as PublicKey + compressed: getBytes(wallet.signingKey.compressedPublicKey) + }) } afterEach(() => jest.restoreAllMocks()) @@ -111,7 +108,7 @@ describe("EthereumNodeOwnerClient", () => { tokenId: EthereumNodeOwnerTier.T2, wireAccountName: WireAccount, wirePublicKey: wirePublicKey(wallet), - depositorPublicKey: wallet._signingKey().publicKey + depositorPublicKey: wallet.signingKey.publicKey }) expect(submission).toEqual({ @@ -129,7 +126,7 @@ describe("EthereumNodeOwnerClient", () => { EthereumNodeOwnerTier.T2, WireAccountName, expect.objectContaining({ keyType: 1 }), - ethersUtils.arrayify(wallet._signingKey().publicKey) + getBytes(wallet.signingKey.publicKey) ) expect(commitTransaction.wait).toHaveBeenCalledWith(1) }) @@ -143,7 +140,7 @@ describe("EthereumNodeOwnerClient", () => { tokenId: EthereumNodeOwnerTier.T2, wireAccountName: WireAccount, wirePublicKey: wirePublicKey(wallet), - depositorPublicKey: wallet._signingKey().publicKey + depositorPublicKey: wallet.signingKey.publicKey }) ).resolves.toEqual( expect.objectContaining({ @@ -158,7 +155,7 @@ describe("EthereumNodeOwnerClient", () => { const { wallet, bar } = contractFixtures(), providerClient = new EthereumNodeOwnerClient( bar, - new providers.JsonRpcProvider() + new JsonRpcProvider() ), signerClient = new EthereumNodeOwnerClient(bar, wallet), otherWallet = Wallet.createRandom(), @@ -166,7 +163,7 @@ describe("EthereumNodeOwnerClient", () => { tokenId: EthereumNodeOwnerTier.T2, wireAccountName: WireAccount, wirePublicKey: wirePublicKey(wallet), - depositorPublicKey: wallet._signingKey().publicKey + depositorPublicKey: wallet.signingKey.publicKey } await expect(providerClient.commit(request)).rejects.toThrow( @@ -175,15 +172,14 @@ describe("EthereumNodeOwnerClient", () => { await expect( signerClient.commit({ ...request, - depositorPublicKey: otherWallet._signingKey().publicKey + depositorPublicKey: otherWallet.signingKey.publicKey }) ).rejects.toThrow("does not belong to the EVM signer") expect(bar.commitNode).not.toHaveBeenCalled() }) it("fails closed when BAR has no canonical WireNodes contract", async () => { - const { wallet, bar } = contractFixtures() - bar.wireNodesContract = jest.fn(async () => ethersConstants.AddressZero) + const { wallet, bar } = contractFixtures(true, ZeroAddress) await expect( new EthereumNodeOwnerClient(bar, wallet).canonicalTokenContractAddress() diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bec2e79..421e7c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -202,6 +202,9 @@ importers: '@wireio/outpost-solana-artifacts': specifier: 0.2.1 version: 0.2.1 + '@wireio/sdk-core': + specifier: workspace:* + version: link:../sdk-core ethers: specifier: ^6.15.0 version: 6.17.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)