@epicuri/atlas-sdk
v0.1.0
Published
The Atlas SDK provides a thin TypeScript client for executing Atlas Move entry functions and view calls over the Aptos blockchain. Phase 1 focuses on the core client surface – a single `AtlasClient` exposing `entry` and `view` primitives, strongly typed e
Readme
Atlas SDK (Phase 1 Skeleton)
The Atlas SDK provides a thin TypeScript client for executing Atlas Move entry functions and view calls over the Aptos blockchain. Phase 1 focuses on the core client surface – a single AtlasClient exposing entry and view primitives, strongly typed error reporting, and transaction policy controls aligned with the Epicuri Atlas plan.
Installation & Configuration
pnpm install
pnpm --filter @epicuri/atlas-sdk buildPublishing to npm
The package is intended to be public on npm. After merging the desired changes into main, publish with:
- Authenticate once via
npm login(or ensure your npm token is available). - Run
pnpm run release:publishfrom a clean checkout.
The script builds the SDK and invokes pnpm publish --access public, which uploads the @epicuri/atlas-sdk tarball to the public npm registry.
AtlasClient wraps the official @aptos-labs/ts-sdk. You can pass any AptosSettings when constructing the client, alongside optional defaults for transaction ordering, submission mode, maximum gas, and a default signer.
import { AtlasClient } from "@epicuri/atlas-sdk";
import { AptosConfig, Account, Network } from "@aptos-labs/ts-sdk";
const config = new AptosConfig({ network: Network.DEVNET });
const alice = Account.generate();
const client = new AtlasClient({
aptos: config,
signer: alice,
txOrdering: "ordered",
});Entry Calls
entry executes Move entry functions using the official Aptos transaction pipeline. It returns a Result that either contains a submitted transaction or a simulation result, or an AtlasError variant when something goes wrong.
const result = await client.entry(
"0x1::aptos_account::transfer",
[],
["0x2", 1_000_000],
);
if (result.ok) {
console.log("Submitted", result.value.hash);
} else {
console.error("Entry failed", result.error);
}SubmitOpts
SubmitOpts can be provided per call to override the client defaults:
txOrdering:'ordered' | 'nonordered'(defaults to the client setting). When'nonordered', the SDK injects a cryptographically strong randomu64replay protection nonce generated from 8 secure random bytes.mode:'submit' | 'simulate'(defaults to'submit').'simulate'builds and simulates the transaction without submitting it.maxGas: Optionalbigintthat limits the maximum gas amount.signer: Overrides the signer used to build and sign the transaction.feePayerAddress/feePayerSigner: Provide a sponsored transaction configuration. Whenmode='submit', both fields must be set and the signer’s address must match the provided fee payer address.settlement:'committed' | 'pending'(defaults to'committed').'committed'waits for execution and surfaces the committed transaction, including events and gas usage.'pending'returns immediately after submission (legacy behavior).waitTimeoutMs: Maximum time in milliseconds to wait for commitment whensettlement='committed'(default30000).pollIntervalMs: Interval in milliseconds between status checks while waiting for commitment (default500).
When settlement='committed' (the default), AtlasClient.entry polls the fullnode and resolves once the transaction is finalized. The returned SubmittedTx carries the pending response and the committed transaction payload, making emitted events immediately available. If the timeout elapses before commitment, the client returns SettlementTimeout { hash, waitedMs }.
const result = await client.entry(
"0x1::aptos_account::transfer",
[],
["0x2", 1_000_000],
{ signer: alice },
);
if (result.ok && result.value.settlement === "committed") {
console.log("Events", result.value.committed.events);
}Switch back to the legacy pending handle if you want to manage settlement yourself:
const pending = await client.entry(
"0x1::aptos_account::transfer",
[],
["0x2", 1_000_000],
{ signer: alice, settlement: "pending" },
);
if (pending.ok) {
console.log("Pending hash", pending.value.hash);
}Example fee-payer flow covering both simulation and submission:
const alice = Account.generate();
const sponsor = Account.generate();
// Dry-run the transaction (no on-chain submission) while validating sponsor data.
await client.entry(
"0x1::aptos_account::transfer",
[],
["0x2", 1_000_000],
{
mode: "simulate",
signer: alice,
feePayerAddress: sponsor.accountAddress.toString(),
feePayerSigner: sponsor,
},
);
// After reviewing simulation results, submit using the same sponsor.
await client.entry(
"0x1::aptos_account::transfer",
[],
["0x2", 1_000_000],
{
mode: "submit",
signer: alice,
feePayerAddress: sponsor.accountAddress.toString(),
feePayerSigner: sponsor,
},
);Transaction Ordering
- Ordered (default) – Transactions rely on the on-chain sequence number. The SDK lets the Aptos TS SDK discover the current sequence during build.
- Non-ordered – Atlas generates a secure random
u64replay protection nonce. Aptos’ orderless pipeline accepts the transaction without advancing the sender’s sequence number, enabling parallel submissions.
View Calls
view proxies to aptos.view and always returns a Result containing the raw Move values returned by the chain. Any simulate flag in the optional argument is ignored (views are read-only by definition per SPEC).
const viewResult = await client.view("0x1::coin::balance", ["0x1::aptos_coin::AptosCoin"], [alice.accountAddress.toString()]);
if (viewResult.ok) {
const [balance] = viewResult.value;
console.log(balance);
}Error Model
All failures surface through the AtlasError discriminated union:
MissingSigner,MissingFeePayerSigner, andFeePayerSignerMismatchcapture user configuration issues.Abortnormalizes Move aborts (module, code, message) from simulations or submissions.SdkErrorcategorizes common Aptos SDK failures (api,network,serialization,timeout,input,unknown).Unknownpreserves any unexpected inputs for later debugging.
Successful responses wrap either SubmittedTx (pending or committed submission metadata) or SimulationResult (an array of simulated user transactions) inside the shared Result<T, E> helpers (ok, err).
Phase 1 Scope
This package intentionally omits higher-level module wrappers, domain helpers, tests, or CI wiring. Future phases will layer additional ergonomics and coverage in line with the published Atlas development plan.
Testing and Guarantees
pnpm buildvalidates the TypeScript build output matches what ships fromsrc/**.pnpm run abi:check(automatically invoked frompnpm test) compares the on-chain ABI at0x9531…1557with the wrapper expectations listed indocs/contracts/ABI_TABLE.md.pnpm testcompiles the test tree (tsconfig.tests.json) and runs the Node test runner across unit, wrapper, domain, and localnet smoke suites. The localnet tests assume a running fullnode athttp://127.0.0.1:8080/v1with a faucet athttp://127.0.0.1:8083and the Atlas modules published to0x95314e637120178029aa8541f24336d2061ccfe84b5d64af6372322d09cf1557.- The funded signer used by the tests is managed in
tests/support/localnet.ts; update that file only when rotating credentials. Do not edit production sources undersrc/**to satisfy tests—raise an escalation if the implementation and spec disagree. - Whenever a test highlights an ambiguity or regression, stop and clarify expectations with the spec owner rather than patching the runtime code. The runtime remains the source of truth; the test suite enforces that contract without redefining it.
Module Wrappers (Phase 2)
Phase 2 layers thin TypeScript wrappers over every public entry and view in the Atlas core modules. Each wrapper:
- accepts an
AtlasClientinstance and a singleatlasModuleAddressin the constructor, reusing that address to build fully qualified function names; - exposes camel-cased methods that mirror the Move entry/view name (for example
append_step→appendStep); - expects an
argsobject as the first parameter, followed by optionaltypeArgs(forwarded unchanged) and the usualSubmitOptsfor entries; - returns a
Resultso the caller can pattern-match on.ok/.errorwithout throwing; - reshapes frequent view payloads:
option::Option<T>becomesT | null,SimpleMap<String, String>becomesRecord<string, string>, andvector<Object<...>>becomes an array of object addresses (string[]).
Constructing wrappers
import { AtlasClient, ActionModule } from "@epicuri/atlas-sdk";
const client = new AtlasClient({ signer: aliceAccount });
const atlasModuleAddress = "0x42...";
const action = new ActionModule(client, atlasModuleAddress);Instantiate whichever module classes you need and keep them alongside your configured AtlasClient for reuse.
Module quickstart
Each snippet below shows one entry call and one view call per module. Replace sample addresses with live object addresses and supply the precise generic type arguments your Move function requires.
import {
ActionModule,
CapabilityModule,
DescriptorModule,
EdgeModule,
EntityModule,
FunctionModule,
ListenerModule,
MessageModule,
NamespaceModule,
RelationshipModule,
TagModule,
TopicModule,
SubscriptionModule,
} from "@epicuri/atlas-sdk";
const atlasModuleAddress = "0x42...";
const action = new ActionModule(client, atlasModuleAddress);
await action.appendStep(
{
action: "0xa11ce",
name: "ingest",
description: "Pull raw payload",
functionObject: "0xf001",
},
["0x1::atlas::Source", "0x1::atlas::Target"],
{ signer: aliceAccount },
);
const actionTags = await action.tags({ action: "0xa11ce" }, ["0x1::atlas::Source", "0x1::atlas::Target"]);
const capability = new CapabilityModule(client, atlasModuleAddress);
await capability.updateRequestSchema(
{ capability: "0xcap", schema: "{"type":"object"}" },
{ signer: aliceAccount },
);
const capabilityDescription = await capability.description({ capability: "0xcap" });
const descriptor = new DescriptorModule(client, atlasModuleAddress);
await descriptor.addTag(
{ descriptor: "0xdesc", tag: "0xtag" },
["0x1::atlas::Schema"],
{ signer: aliceAccount },
);
const descriptorTags = await descriptor.getSupportedTags({ descriptor: "0xdesc" }, ["0x1::atlas::Schema"]);
const edge = new EdgeModule(client, atlasModuleAddress);
await edge.setStatusFlat(
{ edge: "0xed9e", status: 1 },
["0x1::atlas::Source", "0x1::atlas::Target"],
{ signer: aliceAccount },
);
const edgeNamespace = await edge.namespace({ edge: "0xed9e" }, ["0x1::atlas::Source", "0x1::atlas::Target"]);
const entity = new EntityModule(client, atlasModuleAddress);
await entity.addPublishedTopic(
{ entity: "0xent", topic: "0xt0pic" },
{ signer: aliceAccount },
);
const entitySlug = await entity.slugName({ entity: "0xent" });
const func = new FunctionModule(client, atlasModuleAddress);
await func.setStatus(
{ functionObject: "0xfunc", retire: true },
{ signer: aliceAccount },
);
const funcOwner = await func.owner({ functionObject: "0xfunc" });
const listener = new ListenerModule(client, atlasModuleAddress);
await listener.upsertParameter(
{ listener: "0x1ist", key: "region", value: "eu-west" },
{ signer: aliceAccount },
);
const listenerParams = await listener.parameters({ listener: "0x1ist" });
const message = new MessageModule(client, atlasModuleAddress);
await message.publishMessage(
{
listener: "0x1ist",
entity: "0x3nt1ty",
payloadObject: "0xpay10ad",
correlationId: "corr-123",
},
["0x1::atlas::Schema"],
{ signer: aliceAccount },
);
const occurredAt = await message.occurredAt({ message: "0xm3ss" }, ["0x1::atlas::Schema"]);
const namespace = new NamespaceModule(client, atlasModuleAddress);
await namespace.verifyNamespace({ namespace: "0xns" }, { signer: aliceAccount });
const namespaceOwner = await namespace.owner({ namespace: "0xns" });
const relationship = new RelationshipModule(client, atlasModuleAddress);
await relationship.addTag(
{ relationship: "0xrel", tag: "0xtag" },
["0x1::atlas::Source", "0x1::atlas::Target"],
{ signer: aliceAccount },
);
const relationshipEdge = await relationship.getEdge({ relationship: "0xrel" }, ["0x1::atlas::Source", "0x1::atlas::Target"]);
const tag = new TagModule(client, atlasModuleAddress);
await tag.updateStatus(
{ tag: "0xtag", newStatus: 1 },
{ signer: aliceAccount },
);
const promoted = await tag.promoteTag({ tag: "0xtag" }, { signer: aliceAccount });
// Tag module has no public views in this branch.
const topic = new TopicModule(client, atlasModuleAddress);
await topic.addMessageSchemaVersion(
{ topic: "0xt0pic", schema: "{"version":2}" },
{ signer: aliceAccount },
);
const topicStatus = await topic.status({ topic: "0xt0pic" });
const subscription = new SubscriptionModule(client, atlasModuleAddress);
await subscription.addTag(
{ subscription: "0xsub", tag: "0xtag" },
{ signer: aliceAccount },
);
const subscriptionTags = await subscription.allTags({ subscription: "0xsub" });
if (actionTags.ok) {
console.log("Action tags", actionTags.value);
}The result helper utilities (isOk, isErr, etc.) remain available for callers that prefer guard-style handling. Options surface as null, map results become standard objects, and lists of Move Object<T> values always hydrate as a vector of canonical string addresses.
Domain Layer (Phase 3)
Phase 3 introduces optional domain services that compose the one-to-one wrappers into higher level workflows. Each service accepts an AtlasClient plus the Atlas module address so the underlying wrappers remain fully transparent.
import {
AtlasClient,
EntitiesService,
MessagesService,
} from "@epicuri/atlas-sdk";
const atlasModuleAddress = "0x42...";
const client = new AtlasClient({ signer: aliceAccount });
const entities = new EntitiesService(client, { atlasModuleAddress });
const messages = new MessagesService(client, { atlasModuleAddress });Strategies and DomainOpts
Domain methods accept an optional DomainOpts object:
type DomainOpts = {
strategy?: "all_or_nothing" | "best_effort"; // default is "all_or_nothing"
opts?: SubmitOpts; // forwarded verbatim to every wrapper invocation in the sequence
};all_or_nothing(default) stops at the first failing step and setsfailedAtto the zero-based index.best_effortruns the full sequence and records individual failures while leavingfailedAtasnull.
Pass the same SubmitOpts you would use with the wrappers – the domain layer never mutates them. When SubmitOpts.mode === 'simulate' every step stays in simulation mode.
EntitiesService Example
const result = await entities.createWithSetup(
{
slugName: "atlas_operator",
displayName: "Atlas Operator",
description: "Primary operator entity",
tags: ["0xTAG1", "0xTAG2"],
descriptor: "0xDESC",
descriptorSchema: "0xSCHEMA",
listener: { address: "0xLISTENER" },
},
{
strategy: "all_or_nothing",
opts: { signer: aliceAccount },
},
["0x1::atlas::Schema"],
);
if (!result.steps.at(-1)?.result.ok) {
console.error("Entity setup failed", result);
}descriptorTypeArgs and descriptorSchema are required whenever descriptor is supplied. The domain method derives the created entity’s object address and fans out tag/listener calls for you while preserving the original SubmitOpts.
MessagesService Example
const publish = await messages.publish(
{
listener: "0xLISTENER",
entity: "0xENTITY",
payloadObject: "0xPAYLOAD",
correlationId: "1234-5678",
},
{
strategy: "best_effort",
opts: { mode: "simulate", signer: aliceAccount },
},
["0x1::atlas::Schema"],
);
if (publish.steps[0].result.ok) {
console.log("Message published", publish.steps[0].result.value);
}Inspecting DomainBatchResult
Every domain call returns a DomainBatchResult:
type DomainBatchResult = {
strategy: "all_or_nothing" | "best_effort";
steps: Array<{
fqfn: string;
typeArgs: string[];
args: unknown[];
result: Result<SubmittedTx | SimulationResult | unknown, AtlasError>;
}>;
failedAt: number | null;
};Use steps to audit each wrapper call, including the fully qualified function name, the exact arguments forwarded, and the raw Result from the underlying module. This lets you handle partial success (e.g., in best_effort mode) without sacrificing detailed error reporting.
