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
13 changes: 13 additions & 0 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"nswag.consolecore": {
"version": "14.7.1",
"commands": [
"nswag"
],
"rollForward": false
}
}
}
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
~/.cargo/registry
~/.cargo/git
.wasm-build
key: wasm-${{ runner.os }}-${{ hashFiles('scripts/build-wasm.sh', 'rust-toolchain.toml') }}
key: wasm-${{ runner.os }}-${{ hashFiles('scripts/build-wasm.sh', 'scripts/fetch-crate.sh', 'scripts/pins.env', 'rust-toolchain.toml') }}

- name: Set up Node.js
uses: actions/setup-node@v4
Expand Down Expand Up @@ -136,7 +136,7 @@ jobs:
~/.cargo/registry
~/.cargo/git
.wasm-build
key: wasm-${{ runner.os }}-${{ hashFiles('scripts/build-wasm.sh', 'rust-toolchain.toml') }}
key: wasm-${{ runner.os }}-${{ hashFiles('scripts/build-wasm.sh', 'scripts/fetch-crate.sh', 'scripts/pins.env', 'rust-toolchain.toml') }}

- name: Build wasm core
run: bash scripts/build-wasm.sh
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ coverage*.info
# Wasm core built from the pinned crates.io release (never committed)
wasm/
.wasm-build/
.node-api-build/

# Node harness build output
node_modules/
Expand Down
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<ItemGroup>
<PackageVersion Include="Wasmtime" Version="44.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="10.0.2" />
</ItemGroup>

<ItemGroup Label="Test">
Expand Down
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help developer restore build rebuild do-lint do-lint-ci test pack clean wasm node-harness release
.PHONY: help developer restore build rebuild do-lint do-lint-ci test pack clean wasm node-api node-harness release

# Build configuration (Debug or Release)
CONFIG ?= Release
Expand Down Expand Up @@ -65,6 +65,10 @@ pack: build
wasm:
./scripts/build-wasm.sh

# Regenerate the node REST transport from the pinned OpenAPI spec (committed)
node-api:
./scripts/generate-node-api.sh

$(WASM_DEST):
./scripts/build-wasm.sh

Expand Down Expand Up @@ -103,6 +107,7 @@ help:
@echo " make do-lint - Lint code with formatting fixes (C# + harness + spelling)"
@echo " make pack - Produce the NuGet packages into $(ARTIFACTS)/"
@echo " make wasm - Build the P1 wasm core from the pinned crates.io release"
@echo " make node-api - Regenerate the node REST transport from the pinned OpenAPI spec"
@echo " make clean - Remove build outputs"
@echo ""
@echo "CI Commands:"
Expand Down
36 changes: 18 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,12 @@ using KeetaNet.Anchor.Crypto;
using var runtime = WasmRuntime.Load();
```

The runtime exposes one factory per domain `Accounts`, `Certificates`, `KycCertificates`, `Containers`, `Sharables` plus `CreateKycClient` and `CreateAssetMovementClient` for the networked clients. Every handle-backed object they create implements `IDisposable`. Wrap them in `using` and dispose them before the runtime.
The runtime exposes one factory per domain - `Accounts`, `Certificates`, `KycCertificates`, `Containers`, `Sharables` - plus `CreateKycClient` and `CreateAssetMovementClient` for the networked clients. Every handle-backed object they create implements `IDisposable`. Wrap them in `using` and dispose them before the runtime.

In an application using `Microsoft.Extensions.DependencyInjection`, register the runtime once instead of threading it around; the container owns its disposal:

```csharp
// Program.cs requires the KeetaNet.Anchor.Extensions.DependencyInjection package.
// Program.cs - requires the KeetaNet.Anchor.Extensions.DependencyInjection package.
builder.Services.AddKeetaNetAnchor();

// Any service: inject the singleton and create what you need per use.
Expand Down Expand Up @@ -107,20 +107,20 @@ Provider results use the pending-or-ready shape: `Ready` carries the value, othe
using KycClient kyc = runtime.CreateKycClient(nodeUrl, root, signer);

string[] countries = { "US" };
IReadOnlyList<KycProvider> providers = await kyc.GetProvidersAsync(countries, cancellationToken);
IReadOnlyList<KycProvider> providers = await kyc.GetProviders(countries, cancellationToken);
KycProvider provider = providers[0];

// Start a verification. The user completes it in a browser at WebUrl.
VerificationOutcome created = await kyc.StartVerificationAsync(provider, countries, cancellationToken: cancellationToken);
VerificationOutcome created = await kyc.StartVerification(provider, countries, cancellationToken: cancellationToken);
Verification verification = created.Ready!;
Console.WriteLine(verification.WebUrl);
Console.WriteLine(verification.ExpectedCost.Token);

// Poll the provider's decision.
StatusOutcome status = await kyc.GetVerificationStatusAsync(provider, verification.Id, cancellationToken);
StatusOutcome status = await kyc.GetVerificationStatus(provider, verification.Id, cancellationToken);

// Fetch the issued chain once ready; a pending fetch reports RetryAfterMs instead.
CertificatesOutcome outcome = await kyc.GetCertificatesAsync(provider, verification.Id, cancellationToken);
CertificatesOutcome outcome = await kyc.GetCertificates(provider, verification.Id, cancellationToken);
Certificates issued = outcome.Ready!;
string leafPem = issued.Results[0].Value;
```
Expand Down Expand Up @@ -235,39 +235,39 @@ byte[]? signerKey = opened.GetSigningAccount(); // type-prefixed public key, nul
using AssetMovementClient assets = runtime.CreateAssetMovementClient(nodeUrl, root, signer);

// Discovery: all providers, by id, by signer account, or by transfer shape.
IReadOnlyList<AssetProvider> providers = await assets.GetProvidersAsync(cancellationToken);
IReadOnlyList<AssetProvider> providers = await assets.GetProviders(cancellationToken);
var search = new AssetProviderSearch(Asset: asset, From: "chain:evm:100", To: "chain:keeta:100");
IReadOnlyList<AssetProvider> capable = await assets.GetProvidersForTransferAsync(search, cancellationToken);
IReadOnlyList<AssetProvider> capable = await assets.GetProvidersForTransfer(search, cancellationToken);
AssetProvider provider = capable[0];

// Check the signer's readiness before transacting.
AssetAccountStatus account = await assets.GetAccountStatusAsync(provider, cancellationToken);
AssetAccountStatus account = await assets.GetAccountStatus(provider, cancellationToken);

// Push transfer: simulate first, then promote the simulation.
var request = new AssetTransferRequest(
Asset: asset,
From: new AssetTransferSource("chain:evm:100"),
To: new AssetTransferDestination("chain:keeta:100", recipientAddress),
Value: "100");
AssetSimulatedTransfer simulated = await assets.SimulateTransferAsync(provider, request, cancellationToken);
AssetTransfer transfer = await simulated.CreateTransferAsync(cancellationToken: cancellationToken);
AssetTransferStatus status = await transfer.GetTransferStatusAsync(cancellationToken);
AssetSimulatedTransfer simulated = await assets.SimulateTransfer(provider, request, cancellationToken);
AssetTransfer transfer = await simulated.CreateTransfer(cancellationToken: cancellationToken);
AssetTransferStatus status = await transfer.GetTransferStatus(cancellationToken);

// Pull transfer (fiat rails): initiate, pick an instruction, execute.
AssetTransfer pull = await assets.InitiateTransferAsync(provider, pullRequest, cancellationToken);
AssetTransfer pull = await assets.InitiateTransfer(provider, pullRequest, cancellationToken);
var instruction = new AssetPullInstruction("ACH_DEBIT", pull.InstructionChoices[0].GetProperty("pullFrom"));
AssetTransferStatus executed = await pull.ExecuteTransferAsync(instruction, cancellationToken);
AssetTransferStatus executed = await pull.ExecuteTransfer(instruction, cancellationToken);

// Share KYC attributes; a pending outcome polls its promise URL inside the core.
AssetShareKycOutcome shared = await assets.ShareKycAttributesAndWaitAsync(
AssetShareKycOutcome shared = await assets.ShareKycAttributesAndWait(
provider,
new AssetShareKycRequest(exportedAttributes),
pollInterval: TimeSpan.FromSeconds(1),
timeout: TimeSpan.FromMinutes(2),
cancellationToken: cancellationToken);
```

Persistent forwarding follows the same pattern: `InitiatePersistentForwardingTemplateAsync` / `CreatePersistentForwardingTemplateAsync` / `CreatePersistentForwardingAddressAsync` create, the `List*Async` methods page, and the `Deactivate*Async` methods retire.
Persistent forwarding follows the same pattern: `InitiatePersistentForwardingTemplate` / `CreatePersistentForwardingTemplate` / `CreatePersistentForwardingAddress` create, the `List*` methods page, and the `Deactivate*` methods retire.

### Errors

Expand All @@ -276,7 +276,7 @@ Every failure surfaced from the core throws `KeetaException`: a stable machine-r
```csharp
try
{
await assets.InitiateTransferAsync(provider, request, cancellationToken);
await assets.InitiateTransfer(provider, request, cancellationToken);
}
catch (KeetaException error)
{
Expand All @@ -295,7 +295,7 @@ Interop is Wasmtime-hosted WebAssembly, not native P/Invoke. Guest memory is a s
### SDK Rules:

- **Thread-safe by construction.** A `WasmRuntime` owns one Wasmtime `Store`, which cannot be used from more than one thread. The runtime confines it to a dedicated dispatcher thread and serializes every call onto it, so any thread may use the SDK: offline operations dispatch synchronously, networked client operations are `async` and accept a `CancellationToken`.
- **Deterministic disposal.** Handle-backed types (`Account`, `Certificate`, `KycCertificate`, containers, the clients) implement `IDisposable` and release their wasm handle on `Dispose`. Wrap them in `using`. A finalizer backstop reclaims forgotten handles by enqueueing the free onto the dispatcher, but deterministic disposal remains the contract.
- **Deterministic disposal.** Handle-backed types (`Account`, `Certificate`, `KycCertificate`, containers, the clients) implement `IDisposable` and release their wasm handle on `Dispose`. Wrap them in `using`.

## License

Expand Down
5 changes: 5 additions & 0 deletions cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,10 @@ words:
- belgrave
- idisp
- lifecycles
- renderable
- tlsv
- csclient
- nswag
- rasn
- rustfmt
- userprofile
Expand All @@ -66,3 +69,5 @@ words:
- msys
- maxdepth
- powershell
- renderable
- solana
50 changes: 8 additions & 42 deletions scripts/build-wasm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,17 @@
# the library embeds it.
set -euo pipefail

CRATE_NAME="keetanetwork-anchor-client-wasi"
CRATE_VERSION="0.1.1"
CRATE_SHA256="dd5c19da144a320d23d5b0b1af392ea6c94f6b7cb584e490bcf42155c768d9d6"

ROOT="$(cd "$(dirname "$0")/.." && pwd)"

# shellcheck source=scripts/pins.env
source "${ROOT}/scripts/pins.env"
# shellcheck source=scripts/fetch-crate.sh
source "${ROOT}/scripts/fetch-crate.sh"

BUILD_DIR="${ROOT}/.wasm-build"
TARBALL="${BUILD_DIR}/${CRATE_NAME}-${CRATE_VERSION}.crate"
CRATE_DIR="${BUILD_DIR}/${CRATE_NAME}-${CRATE_VERSION}"
CRATE_DIR="${BUILD_DIR}/${ANCHOR_WASI_CRATE}-${ANCHOR_WASI_VERSION}"
ARTIFACT="${CRATE_DIR}/target/wasm32-wasip1/release/keetanetwork_anchor_client_wasi.wasm"
DEST="${ROOT}/src/KeetaNet.Anchor/wasm/keetanetwork_anchor_client_wasi.wasm"
DOWNLOAD_URL="https://crates.io/api/v1/crates/${CRATE_NAME}/${CRATE_VERSION}/download"

checksum() {
local file="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "${file}" | cut -d' ' -f1
else
shasum -a 256 "${file}" | cut -d' ' -f1
fi
}

# rasn-compiler probes `$CARGO_HOME/bin/rustfmt` and `$CARGO`-adjacent
# `rustfmt` without the .exe suffix. When both miss it silently emits
Expand Down Expand Up @@ -102,31 +93,6 @@ require_wasip1_target() {
fi
}

fetch_crate() {
mkdir -p "${BUILD_DIR}"
if [[ ! -f "${TARBALL}" ]]; then
echo "build-wasm: downloading ${CRATE_NAME} ${CRATE_VERSION} from crates.io"
# crates.io rejects requests without a User-Agent.
curl --fail --silent --show-error --location \
--proto '=https' --tlsv1.2 \
--user-agent "anchor-csharp build-wasm (https://github.com/KeetaNetwork/anchor-csharp)" \
--output "${TARBALL}" "${DOWNLOAD_URL}"
fi

actual="$(checksum "${TARBALL}")"
if [[ "${actual}" != "${CRATE_SHA256}" ]]; then
rm -f "${TARBALL}"
echo "build-wasm: checksum mismatch for ${TARBALL}" >&2
echo " expected: ${CRATE_SHA256}" >&2
echo " actual: ${actual}" >&2
exit 1
fi

if [[ ! -d "${CRATE_DIR}" ]]; then
tar --extract --gzip --file "${TARBALL}" --directory "${BUILD_DIR}"
fi
}

build_artifact() {
echo "build-wasm: cargo build (wasm32-wasip1, features p1, release)"
cargo build \
Expand All @@ -139,7 +105,7 @@ build_artifact() {

shim_rustfmt_for_windows
require_wasip1_target
fetch_crate
fetch_crate "${ANCHOR_WASI_CRATE}" "${ANCHOR_WASI_VERSION}" "${ANCHOR_WASI_SHA256}" "${BUILD_DIR}"
purge_stale_asn1_outputs
build_artifact

Expand Down
46 changes: 46 additions & 0 deletions scripts/fetch-crate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Shared pinned-crate fetch for the generation scripts. Source this file and
# call `fetch_crate`.

checksum() {
local file="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "${file}" | cut -d' ' -f1
else
shasum -a 256 "${file}" | cut -d' ' -f1
fi
}

# fetch_crate <name> <version> <sha256> <build_dir>
#
# Download the pinned crates.io release into `<build_dir>`, verify its SHA-256,
# and extract it to `<build_dir>/<name>-<version>` (skipping work already done).
fetch_crate() {
local name="$1" version="$2" expected="$3" build_dir="$4"
local tarball="${build_dir}/${name}-${version}.crate"
local crate_dir="${build_dir}/${name}-${version}"
local url="https://crates.io/api/v1/crates/${name}/${version}/download"

mkdir -p "${build_dir}"
if [[ ! -f "${tarball}" ]]; then
echo "fetch-crate: downloading ${name} ${version} from crates.io"
# crates.io rejects requests without a User-Agent.
curl --fail --silent --show-error --location \
--proto '=https' --tlsv1.2 \
--user-agent "anchor-csharp fetch-crate (https://github.com/KeetaNetwork/anchor-csharp)" \
--output "${tarball}" "${url}"
fi

local actual
actual="$(checksum "${tarball}")"
if [[ "${actual}" != "${expected}" ]]; then
rm -f "${tarball}"
echo "fetch-crate: checksum mismatch for ${tarball}" >&2
echo " expected: ${expected}" >&2
echo " actual: ${actual}" >&2
exit 1
fi

if [[ ! -d "${crate_dir}" ]]; then
tar --extract --gzip --file "${tarball}" --directory "${build_dir}"
fi
}
39 changes: 39 additions & 0 deletions scripts/generate-node-api.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env bash
# Generate the node REST transport (models + typed operations) from the
# canonical OpenAPI spec shipped in the pinned keetanetwork-client crate.
set -euo pipefail

ROOT="$(cd "$(dirname "$0")/.." && pwd)"

# shellcheck source=scripts/pins.env
source "${ROOT}/scripts/pins.env"
# shellcheck source=scripts/fetch-crate.sh
source "${ROOT}/scripts/fetch-crate.sh"

BUILD_DIR="${ROOT}/.node-api-build"
CRATE_DIR="${BUILD_DIR}/${NODE_CLIENT_CRATE}-${NODE_CLIENT_VERSION}"
SPEC="${CRATE_DIR}/openapi/keetanet-node.yaml"
DEST="${ROOT}/src/KeetaNet.Anchor/Generated/Node/NodeApi.cs"

generate_client() {
echo "generate-node-api: nswag openapi2csclient from ${SPEC}"
mkdir -p "$(dirname "${DEST}")"
(cd "${ROOT}" && dotnet tool restore >/dev/null && dotnet nswag openapi2csclient \
"/input:${SPEC}" \
/classname:NodeApi \
/namespace:KeetaNet.Anchor.Generated.Node \
"/output:${DEST}" \
/jsonLibrary:SystemTextJson \
/injectHttpClient:true \
/disposeHttpClient:false \
/generateClientInterfaces:false \
/generateOptionalParameters:true \
/generateNativeRecords:true \
/useBaseUrl:true \
/generateBaseUrlProperty:true \
/exceptionClass:NodeApiException)
}

fetch_crate "${NODE_CLIENT_CRATE}" "${NODE_CLIENT_VERSION}" "${NODE_CLIENT_SHA256}" "${BUILD_DIR}"
generate_client
echo "generate-node-api: node transport at ${DEST}"
12 changes: 12 additions & 0 deletions scripts/pins.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Every pinned upstream crate release, in one place. Sourced by the scripts;
# bump versions and checksums here only.

# The wasm core (scripts/build-wasm.sh).
ANCHOR_WASI_CRATE="keetanetwork-anchor-client-wasi"
ANCHOR_WASI_VERSION="0.2.0"
ANCHOR_WASI_SHA256="19c51c46f5efa58bcf4a37e324090bb91644ede5fcb0ae916983214e9092ff42"

# The canonical node OpenAPI spec (scripts/generate-node-api.sh).
NODE_CLIENT_CRATE="keetanetwork-client"
NODE_CLIENT_VERSION="0.4.0"
NODE_CLIENT_SHA256="b43d33dc69ca0571499c19046b016271b66a7f4d9b2aa5539a2351b4c3231270"
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Http" />
</ItemGroup>

<ItemGroup>
Expand Down
Loading
Loading