Skip to content
Merged
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
15 changes: 15 additions & 0 deletions packages/sdk-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,23 @@ await reserves.pushMatchReserve({
matcher: "alice",
wireAmount: pending[0].requestedWireAmount
})

// Several pending rows can be activated atomically in one signed transaction.
await reserves.pushMatchReserves({
matches: pending.map(reserve => ({
chainCode: reserve.chainCode,
tokenCode: reserve.tokenCode,
reserveCode: reserve.reserveCode,
matcher: "alice",
wireAmount: reserve.requestedWireAmount
}))
})
```

`pushMatchReserves` preserves the supplied action order and rejects an empty
match list. The Wire transaction is atomic: either every `matchreserve` action
is accepted or none is applied.

## Reserve swaps

Reserve swap integrations compose three on-chain sources instead of carrying a
Expand Down
5 changes: 5 additions & 0 deletions packages/sdk-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
"types": "./lib/esm/*.d.ts"
}
},
"typesVersions": {
"*": {
"*": ["lib/esm/*"]
}
},
"access": "public",
"license": "FSL-1.1-Apache-2.0",
"scripts": {
Expand Down
22 changes: 21 additions & 1 deletion packages/sdk-core/src/contracts/sysio/reserv/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import type * as SysioContracts from "../../../types/SysioContractTypes.js"
import type { ContractTableRowsOptions } from "../../Contract.js"
import { getSysioContract, type SysioContractClient } from "../Client.js"

import { buildSwapQuoteAction, matchReserveActionData } from "./Actions.js"
import {
buildMatchReserveAction,
buildSwapQuoteAction,
matchReserveActionData
} from "./Actions.js"
import {
DEFAULT_RESERV_CONTRACT,
DEFAULT_RESERVE_QUERY_LIMIT
Expand All @@ -27,6 +31,7 @@ import {
import type {
ListReservesOptions,
PushMatchReserveOptions,
PushMatchReservesOptions,
ReserveClientOptions,
ReserveIdentity,
ReserveQuoteOptions,
Expand Down Expand Up @@ -271,6 +276,21 @@ export class ReserveClient {
)
}

/** Builds and pushes one signed Wire transaction that activates several pending reserves atomically. */
async pushMatchReserves(
options: PushMatchReservesOptions,
pushOptions: TransactionExtraOptions = options.pushOptions || {}
): Promise<Awaited<ReturnType<APIClient["pushTransaction"]>>> {
if (options.matches.length === 0) {
throw new Error("At least one reserve match is required.")
}

return this.client.pushTransaction(
options.matches.map(buildMatchReserveAction),
pushOptions
)
}

/** Reads the current on-chain quote for one reserve route. */
async getSwapQuote(options: ReserveQuoteOptions): Promise<bigint> {
const response = await this.sendReadOnlyAction(
Expand Down
8 changes: 8 additions & 0 deletions packages/sdk-core/src/contracts/sysio/reserv/Types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ export interface PushMatchReserveOptions extends MatchReserveOptions {
pushOptions?: TransactionExtraOptions
}

/** Options for atomically matching several pending reserves on Wire. */
export interface PushMatchReservesOptions {
/** Ordered reserve matches included in one Wire transaction. */
matches: readonly MatchReserveOptions[]
/** Optional push behavior such as finality waiting. */
pushOptions?: TransactionExtraOptions
}

/** Options for a read-only reserve-to-reserve quote. */
export interface ReserveQuoteOptions {
/** Source reserve identity. */
Expand Down
45 changes: 45 additions & 0 deletions packages/sdk-core/tests/contracts/sysio/reserv/Client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,51 @@ describe("ReserveClient", () => {
expect(action.authorization.map(String)).toEqual(["alice@active"])
})

test("pushes ordered reserve matches in one Wire transaction", async () => {
const { client, pushTransaction } = clientFixture()

await expect(
client.pushMatchReserves({
matches: [
{
chainCode: "ETHEREUM",
tokenCode: "ETH",
reserveCode: "PRIVATE",
matcher: "alice",
wireAmount: "2500000000"
},
{
chainCode: "SOLANA",
tokenCode: "USDC",
reserveCode: "PRIVATE",
matcher: "alice",
wireAmount: "2500000000"
}
]
})
).resolves.toEqual({ transaction_id: "reserve-trx" })

const [actions] = pushTransaction.mock.calls[0]
expect(actions).toHaveLength(2)
expect(actions.map(action => action.name.toString())).toEqual([
"matchreserve",
"matchreserve"
])
expect(actions.map(action => action.authorization.map(String))).toEqual([
["alice@active"],
["alice@active"]
])
})

test("rejects an empty atomic reserve match", async () => {
const { client, pushTransaction } = clientFixture()

await expect(client.pushMatchReserves({ matches: [] })).rejects.toThrow(
"At least one reserve match is required."
)
expect(pushTransaction).not.toHaveBeenCalled()
})

test("decodes read-only swapquote and rewards values", async () => {
const { client } = clientFixture()

Expand Down
65 changes: 61 additions & 4 deletions packages/sdk-outpost/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ module entrypoints with TypeScript declarations.
## Supported surfaces

| Family | Generated clients and workflows |
| -------- | ------------------------------------------------------------------------ |
| Ethereum | `OPP`, `OPPInbound`, `OperatorRegistry`, `ReserveManager`, reserve swaps |
| Solana | `liqsol_core`, native SOL and classic SPL reserve swaps |
| -------- | ------------------------------------------------------------------------- |
| Ethereum | `OPP`, `OPPInbound`, `OperatorRegistry`, `ReserveManager`, reserve lifecycle and swaps |
| Solana | `liqsol_core`, configured reserve lifecycle, native SOL and classic SPL reserve swaps |

Client creation verifies all four boundaries before returning:

Expand Down Expand Up @@ -120,6 +120,62 @@ Ethereum also exposes `requestErc20WithApproval`, `nativeBalance`, and
`erc20Balance`. Solana exposes `requestNative`, `requestSpl`, `nativeBalance`,
and `splBalance` through the same `client.swaps` ownership boundary.

## Reserve lifecycle

Wallet-connected clients expose the external half of the post-bootstrap
reserve lifecycle. The external create escrows reserve capital and emits the
attestation that creates a pending `sysio.reserv` row. A signed
`@wireio/sdk-core` `ReserveClient` then supplies the exact requested WIRE amount
and activates that row.

```ts
const ethereumSubmission = await ethereum.reserves.createNative({
tokenCode,
reserveCode,
externalTokenAmount,
requestedWireAmount,
connectorWeightBps: 5_000,
name: "Private ETH reserve",
description: "",
isPrivate: true,
creatorPubKey
})

const configuredTokens = await solana.reserves.getConfiguredTokens()
const splToken = configuredTokens.find(token => !token.isNative)
if (splToken == null) throw new Error("No configured SPL reserve token.")

const solanaSubmission = await solana.reserves.create({
tokenCode: splToken.tokenCode,
reserveCode,
externalTokenAmount: splAmount,
requestedWireAmount,
connectorWeightBps: 5_000,
name: "Private SPL reserve",
description: "",
isPrivate: true,
mint: splToken.mint
})
```

Ethereum supports native creation, ERC-20 approval or permit creation, pending
cancellation, and local reserve reads. Solana supports deployment-configured
token discovery, instruction assembly, creation, pending cancellation, address
derivation, and local reserve reads. `cancel` is valid only while creation is
pending and drives the protocol refund path.

The all-zero mint returned for a configured native SOL route is protocol
metadata, not an Anchor account. The current `create_reserve` account context
still requires a real placeholder SPL mint and the creator's token account for
native SOL creation. Consumers that have not provisioned those accounts should
select a configured non-native SPL route, as in the example above.

Private is a routing constraint, not access control or confidentiality. Private
reserves cannot use WIRE as a swap endpoint; when either external route leg is
private, Wire requires both active reserves to have the same non-empty owner.
The current protocol exposes no creator withdrawal, close, or redemption after
activation. This SDK intentionally does not invent an active-reserve exit API.

Solana uses the same facade and returns the precise Anchor program type at the
runtime program address:

Expand Down Expand Up @@ -155,7 +211,8 @@ repository.
## Consumer boundaries

- Use this package for typed external `ReserveManager`, `OperatorRegistry`,
`OPP`, `OPPInbound`, `liqsol_core`, and source reserve-swap execution.
`OPP`, `OPPInbound`, `liqsol_core`, reserve lifecycle, and source reserve-swap
execution.
- Use `@wireio/sdk-core` for Wire transaction construction, reserve and token
registries, underwriting state, and settlement correlation.
- Recreate external clients whenever the selected deployment profile changes.
Expand Down
1 change: 1 addition & 0 deletions packages/sdk-outpost/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"prepack": "pnpm run verify:release"
},
"dependencies": {
"@wireio/sdk-core": "workspace:*",
"@coral-xyz/anchor": "^0.32.1",
"@ethersproject/abi": "^5.8.0",
"@ethersproject/providers": "^5.8.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "../../deployments/index.js"
import { OutpostDeploymentVerifier } from "../../verification/index.js"
import { EthereumContractMap, EthereumOutpostClientOptions } from "./Types.js"
import { EthereumReserveClient } from "./EthereumReserveClient.js"
import { EthereumReserveSwapClient } from "./EthereumReserveSwapClient.js"

function resolveProvider(
Expand Down Expand Up @@ -47,12 +48,19 @@ export class EthereumOutpostClient {
/** Provider verified against the configured Ethereum chain. */
readonly provider: providers.Provider
) {
this.reserves = new EthereumReserveClient(
this.contract(EthereumContractName.ReserveManager),
options.connection
)
this.swaps = new EthereumReserveSwapClient(
this.contract(EthereumContractName.ReserveManager),
options.connection
)
}

/** Reserve creation, cancellation, and reads for this verified outpost. */
readonly reserves: EthereumReserveClient

/** Reserve-swap writes and balance reads for this verified outpost. */
readonly swaps: EthereumReserveSwapClient

Expand Down
Loading
Loading