Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
0272328
feat(web-client): add ImmutableContract/MutableContract account creation
WiktorStarczewski Feb 25, 2026
5cff9a3
feat(web-client): add CompilerResource (client.compile)
WiktorStarczewski Feb 25, 2026
c9e4aee
feat(web-client): add transactions.execute() for custom script execution
WiktorStarczewski Feb 25, 2026
c2d46b1
types(web-client): add TypeScript declarations for new API surface
WiktorStarczewski Feb 25, 2026
dc9a597
test(web-client): add tests for compile and contract creation API
WiktorStarczewski Feb 25, 2026
70674b5
chore: add changelog entries for #1828
WiktorStarczewski Feb 25, 2026
42b2765
docs(web-client): document contract creation, compiler resource, and …
WiktorStarczewski Feb 25, 2026
4454100
chore: apply prettier formatting
WiktorStarczewski Feb 25, 2026
3e1e78f
chore: regenerate TypeDoc web-client reference
WiktorStarczewski Feb 25, 2026
b75a9ea
fix(web-client): match procedure by local name in getProcedureHash
WiktorStarczewski Feb 25, 2026
90d966d
fix(web-client): use is_some_and to satisfy clippy
WiktorStarczewski Feb 25, 2026
29c9adb
fix(web-client): update procedure hash length assertion for 0x prefix
WiktorStarczewski Feb 26, 2026
0b1ce57
feat(sdk): add NoteVisibility and StorageMode enum constants
WiktorStarczewski Feb 26, 2026
793d9ca
feat(sdk): accept object types directly in API calls (TransactionId, …
WiktorStarczewski Feb 26, 2026
1063242
feat(react-sdk): accept Account/AccountHeader objects in hook options…
WiktorStarczewski Feb 26, 2026
a25bc53
fix(format): run prettier on JS/TS files
WiktorStarczewski Feb 26, 2026
26a6456
fix(react-sdk): widen resolveAccountId to accept AccountRef type
WiktorStarczewski Feb 26, 2026
fef5cfc
fix(web-client): harden SDK dispatch, duck-typing, and error handling
WiktorStarczewski Feb 26, 2026
42590f5
fix(web-client): use StorageMode type alias for storage fields in API…
WiktorStarczewski Feb 26, 2026
688fa6e
chore(web-client): regenerate TypeDoc and add comment to txScript get…
WiktorStarczewski Feb 26, 2026
3bd4468
chore: consolidate changelog entries for #1828
WiktorStarczewski Feb 26, 2026
e736029
docs(web-client): add missing JSDoc comments to MintOptions fields
WiktorStarczewski Feb 26, 2026
85f135e
chore(web-client): regenerate TypeDoc for MintOptions JSDoc changes
WiktorStarczewski Feb 26, 2026
4d78666
fix(web-client): fix 4 failing integration tests in compile_and_contract
WiktorStarczewski Feb 26, 2026
7c74fd7
feat(web-client): extend send() for unauthenticated P2ID notes; remov…
WiktorStarczewski Feb 26, 2026
acee6bc
chore(web-client): regenerate TypeDoc for unauthenticated send API ch…
WiktorStarczewski Feb 26, 2026
4895a5e
fix: run prettier on unformatted files
WiktorStarczewski Feb 26, 2026
194a547
fix(react-sdk): update useSend test to match new SendResult type
WiktorStarczewski Feb 26, 2026
ffb4867
feat(web-client): add prover shorthand resolution to MidenClient.create
WiktorStarczewski Feb 26, 2026
91dc1f7
feat(web-client): add rpcUrl shorthand resolution to MidenClient.create
WiktorStarczewski Feb 26, 2026
8232a53
chore(web-client): regenerate TypeDoc for rpcUrl/proverUrl shorthand …
WiktorStarczewski Feb 26, 2026
cc069e3
refactor(web-client, react-sdk): simplify notes API surface
WiktorStarczewski Mar 2, 2026
5407acb
refactor(web-client, react-sdk): rename authenticated send option to …
WiktorStarczewski Mar 3, 2026
b59d851
refactor(web-client): extract isDirectNote helper to remove duplicate…
WiktorStarczewski Mar 3, 2026
0c922ca
docs(web-client): clarify when to use getOrImport vs get for accounts
WiktorStarczewski Mar 3, 2026
c0e024e
fix(web-client): store secret key after account creation, not before
WiktorStarczewski Mar 3, 2026
031c649
fix: merge use statements for nightly fmt and remove stale Consumable…
WiktorStarczewski Mar 3, 2026
69a46de
chore: run prettier on transactions.js and useConsume.test.tsx
WiktorStarczewski Mar 3, 2026
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
* [FEATURE][web] New `MidenClient` class with resource-based API (`client.accounts`, `client.transactions`, `client.notes`, `client.tags`, `client.settings`). Provides high-level transaction helpers (`send`, `mint`, `consume`, `swap`, `consumeAll`), transaction dry-runs via `preview()`, confirmation polling via `waitFor()`, and flexible account/note references that accept hex strings, bech32 strings, or WASM objects interchangeably (`AccountRef`, `NoteInput` types). Factory methods: `MidenClient.create()`, `MidenClient.createTestnet()`, `MidenClient.createMock()`. ([#1762](https://github.com/0xMiden/miden-client/pull/1762))
* [FEATURE][web] Added `TransactionId.fromHex()` static constructor for creating transaction IDs from hex strings. ([#1762](https://github.com/0xMiden/miden-client/pull/1762))
* [FEATURE][web] Added standalone tree-shakeable note utilities (`createP2IDNote`, `createP2IDENote`, `buildSwapTag`) usable without a client instance. ([#1762](https://github.com/0xMiden/miden-client/pull/1762))
* [FEATURE][web] Custom contract support: `accounts.create()` with `ImmutableContract`/`MutableContract` types, new `client.compile` resource (`compile.component()`, `compile.txScript()` with `"dynamic"`/`"static"` linking), and `transactions.execute({ account, script, foreignAccounts? })` for custom script execution with FPI. ([#1828](https://github.com/0xMiden/miden-client/pull/1828))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

client.trnsaction.send({...}) changed its return type so this changes should be marked as [BREAKING] too.

* [FEATURE][web] Account import improvements: `accounts.getOrImport(ref)` convenience method, and `accounts.import()` now accepts full `AccountRef` (string, `AccountId`, `Account`, `AccountHeader`) in addition to `{ file }` and `{ seed }` forms. ([#1828](https://github.com/0xMiden/miden-client/pull/1828))

## 0.13.1 (TBD)

Expand Down
3 changes: 1 addition & 2 deletions crates/rust-client/src/rpc/domain/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -671,8 +671,7 @@ impl From<AccountStorageRequirements> for Vec<account_detail_request::StorageMap
fn from(
value: AccountStorageRequirements,
) -> Vec<account_detail_request::StorageMapDetailRequest> {
use account_detail_request;
use account_detail_request::storage_map_detail_request;
use account_detail_request::{self, storage_map_detail_request};
let request_map = value.0;
let mut requests = Vec::with_capacity(request_map.len());
for (slot_name, _map_keys) in request_map {
Expand Down
52 changes: 46 additions & 6 deletions crates/web-client/js/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { TransactionsResource } from "./resources/transactions.js";
import { NotesResource } from "./resources/notes.js";
import { TagsResource } from "./resources/tags.js";
import { SettingsResource } from "./resources/settings.js";
import { CompilerResource } from "./resources/compiler.js";
import { hashSeed } from "./utils.js";

/**
Expand Down Expand Up @@ -33,6 +34,7 @@ export class MidenClient {
this.notes = new NotesResource(inner, getWasm, this);
this.tags = new TagsResource(inner, getWasm, this);
this.settings = new SettingsResource(inner, getWasm, this);
this.compile = new CompilerResource(inner, getWasm, this);
}

/**
Expand All @@ -53,10 +55,12 @@ export class MidenClient {

const seed = options?.seed ? await hashSeed(options.seed) : undefined;

const rpcUrl = resolveRpcUrl(options?.rpcUrl);

let inner;
if (options?.keystore) {
inner = await WebClientClass.createClientWithExternalKeystore(
options?.rpcUrl,
rpcUrl,
options?.noteTransportUrl,
seed,
options?.storeName,
Expand All @@ -66,7 +70,7 @@ export class MidenClient {
);
} else {
inner = await WebClientClass.createClient(
options?.rpcUrl,
rpcUrl,
options?.noteTransportUrl,
seed,
options?.storeName
Expand All @@ -76,10 +80,7 @@ export class MidenClient {
let defaultProver = null;
if (options?.proverUrl) {
const wasm = await getWasm();
defaultProver = wasm.TransactionProver.newRemoteProver(
options.proverUrl,
undefined
);
defaultProver = resolveProver(options.proverUrl, wasm);
}

const client = new MidenClient(inner, getWasm, defaultProver);
Expand Down Expand Up @@ -247,3 +248,42 @@ export class MidenClient {
}
}
}

const RPC_URLS = {
testnet: "https://rpc.testnet.miden.io",
devnet: "https://rpc.devnet.miden.io",
localhost: "http://localhost:57291",
local: "http://localhost:57291",
};

/**
* Resolves an rpcUrl shorthand or raw URL into a concrete endpoint string.
*
* @param {string | undefined} rpcUrl - "testnet", "devnet", "localhost", "local", or a raw URL.
* @returns {string | undefined} A fully qualified URL, or undefined to use the SDK default.
*/
function resolveRpcUrl(rpcUrl) {
if (!rpcUrl) return undefined;
return RPC_URLS[rpcUrl.trim().toLowerCase()] ?? rpcUrl;
}

const PROVER_URLS = {
devnet: "https://tx-prover.devnet.miden.io",
testnet: "https://tx-prover.testnet.miden.io",
};

/**
* Resolves a proverUrl shorthand or raw URL into a TransactionProver.
*
* @param {string} proverUrl - "local", "devnet", "testnet", or a raw URL.
* @param {object} wasm - Loaded WASM module.
* @returns {object} A TransactionProver instance.
*/
function resolveProver(proverUrl, wasm) {
const normalized = proverUrl.trim().toLowerCase();
if (normalized === "local") {
return wasm.TransactionProver.newLocalProver();
}
const remoteUrl = PROVER_URLS[normalized] ?? proverUrl;
return wasm.TransactionProver.newRemoteProver(remoteUrl, undefined);
}
30 changes: 30 additions & 0 deletions crates/web-client/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,26 @@ export const AccountType = Object.freeze({
MutableWallet: "MutableWallet",
ImmutableWallet: "ImmutableWallet",
FungibleFaucet: "FungibleFaucet",
ImmutableContract: "ImmutableContract",
MutableContract: "MutableContract",
});

export const AuthScheme = Object.freeze({
Falcon: "falcon",
ECDSA: "ecdsa",
});

export const NoteVisibility = Object.freeze({
Public: "public",
Private: "private",
});

export const StorageMode = Object.freeze({
Public: "public",
Private: "private",
Network: "network",
});

export { MidenClient };
export { createP2IDNote, createP2IDENote, buildSwapTag };

Expand Down Expand Up @@ -473,6 +486,23 @@ class WebClient {
});
}

async newAccount(account, overwrite) {
return this._serializeWasmCall(async () => {
const wasmWebClient = await this.getWasmWebClient();
return await wasmWebClient.newAccount(account, overwrite);
});
}

async addAccountSecretKeyToWebStore(accountId, secretKey) {
return this._serializeWasmCall(async () => {
const wasmWebClient = await this.getWasmWebClient();
return await wasmWebClient.addAccountSecretKeyToWebStore(
accountId,
secretKey
);
});
}

async submitNewTransaction(accountId, transactionRequest) {
try {
if (!this.worker) {
Expand Down
57 changes: 52 additions & 5 deletions crates/web-client/js/resources/accounts.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ export class AccountsResource {
);
}

if (
opts?.type === "ImmutableContract" ||
opts?.type === "MutableContract"
) {
return await this.#createContract(opts, wasm);
}

// Default: wallet (mutable or immutable based on type)
const mutable = resolveAccountMutability(opts?.type);
const storageMode = resolveStorageMode(opts?.storage ?? "private", wasm);
Expand All @@ -42,6 +49,43 @@ export class AccountsResource {
return await this.#inner.newWallet(storageMode, mutable, authScheme, seed);
}

async #createContract(opts, wasm) {
if (!opts.seed)
throw new Error("Contract creation requires a 'seed' (Uint8Array)");
if (!opts.auth)
throw new Error("Contract creation requires an 'auth' (AuthSecretKey)");

const mutable = opts.type === "MutableContract";
const accountTypeEnum = mutable
? wasm.AccountType.RegularAccountUpdatableCode
: wasm.AccountType.RegularAccountImmutableCode;
const storageMode = resolveStorageMode(opts.storage ?? "public", wasm);
const authComponent =
wasm.AccountComponent.createAuthComponentFromSecretKey(opts.auth);

let builder = new wasm.AccountBuilder(opts.seed)
.accountType(accountTypeEnum)
.storageMode(storageMode)
.withAuthComponent(authComponent);

for (const component of opts.components ?? []) {
builder = builder.withComponent(component);
}

const built = builder.build();
const account = built.account;
const accountId = account.id();

await this.#inner.newAccount(account, false);
await this.#inner.addAccountSecretKeyToWebStore(accountId, opts.auth);
return await this.#inner.getAccount(accountId);
}

async getOrImport(ref) {
this.#client.assertNotTerminated();
return (await this.get(ref)) ?? (await this.import(ref));
}

async get(ref) {
this.#client.assertNotTerminated();
const wasm = await this.#getWasm();
Expand Down Expand Up @@ -86,8 +130,10 @@ export class AccountsResource {
this.#client.assertNotTerminated();
const wasm = await this.#getWasm();

if (typeof input === "string") {
// Import by ID (hex or bech32 string)
// Early exit for string, Account, and AccountHeader types before property
// checks, preventing misrouting if a WASM object ever gains a .file or .seed
// property. Bare AccountId (no .id() method) falls through to the fallback.
if (typeof input === "string" || typeof input.id === "function") {
const id = resolveAccountRef(input, wasm);
await this.#inner.importAccountById(id);
return await this.#inner.getAccount(id);
Expand Down Expand Up @@ -121,9 +167,10 @@ export class AccountsResource {
);
}

throw new Error(
"Invalid import input: expected a string, { file }, or { seed }"
);
// Fallback: treat as AccountRef (string, AccountId, Account, AccountHeader)
const id = resolveAccountRef(input, wasm);
await this.#inner.importAccountById(id);
return await this.#inner.getAccount(id);
}

async export(ref) {
Expand Down
57 changes: 57 additions & 0 deletions crates/web-client/js/resources/compiler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
export class CompilerResource {
#inner;
#getWasm;
#client;

constructor(inner, getWasm, client) {
this.#inner = inner;
this.#getWasm = getWasm;
this.#client = client;
}

/**
* Compiles MASM code + slots into an AccountComponent ready for accounts.create().
*
* @param {{ code: string, slots: StorageSlot[] }} opts
* @returns {Promise<AccountComponent>}
*/
async component({ code, slots }) {
this.#client.assertNotTerminated();
const wasm = await this.#getWasm();
const builder = this.#inner.createCodeBuilder();
const compiled = builder.compileAccountComponentCode(code);
return wasm.AccountComponent.compile(
compiled,
slots
).withSupportsAllTypes();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be opt-out?

}

/**
* Compiles a transaction script, optionally linking named libraries inline.
*
* @param {{ code: string, libraries?: Array<{ namespace: string, code: string, linking?: "dynamic" | "static" }> }} opts
* @returns {Promise<TransactionScript>}
*/
async txScript({ code, libraries = [] }) {
this.#client.assertNotTerminated();
// Ensure WASM is initialized (result unused — only #inner needs it)
await this.#getWasm();
const builder = this.#inner.createCodeBuilder();
for (const lib of libraries) {
if (lib && typeof lib.namespace === "string") {
// Inline { namespace, code, linking? } — build and link automatically
const built = builder.buildLibrary(lib.namespace, lib.code);
if (lib.linking === "static") {
builder.linkStaticLibrary(built);
} else {
// Default: "dynamic" — matches existing tutorial behavior
builder.linkDynamicLibrary(built);
}
} else {
// Pre-built library object — link dynamically
builder.linkDynamicLibrary(lib);
}
}
return builder.compileTxScript(code);
}
}
22 changes: 15 additions & 7 deletions crates/web-client/js/resources/notes.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { resolveAccountRef, resolveAddress } from "../utils.js";
import {
resolveAccountRef,
resolveAddress,
resolveNoteIdHex,
} from "../utils.js";

export class NotesResource {
#inner;
Expand All @@ -20,7 +24,7 @@ export class NotesResource {

async get(noteId) {
this.#client.assertNotTerminated();
const result = await this.#inner.getInputNote(noteId);
const result = await this.#inner.getInputNote(resolveNoteIdHex(noteId));
return result ?? null;
}

Expand All @@ -35,7 +39,8 @@ export class NotesResource {
this.#client.assertNotTerminated();
const wasm = await this.#getWasm();
const accountId = resolveAccountRef(opts.account, wasm);
return await this.#inner.getConsumableNotes(accountId);
const consumable = await this.#inner.getConsumableNotes(accountId);
return consumable.map((c) => c.inputNoteRecord());
}

async import(noteFile) {
Expand All @@ -47,7 +52,7 @@ export class NotesResource {
this.#client.assertNotTerminated();
const wasm = await this.#getWasm();
const format = opts?.format ?? wasm.NoteExportFormat.Full;
return await this.#inner.exportNoteFile(noteId, format);
return await this.#inner.exportNoteFile(resolveNoteIdHex(noteId), format);
}

async fetchPrivate(opts) {
Expand All @@ -62,9 +67,10 @@ export class NotesResource {
async sendPrivate(opts) {
this.#client.assertNotTerminated();
const wasm = await this.#getWasm();
const noteRecord = await this.#inner.getInputNote(opts.noteId);
const noteHex = resolveNoteIdHex(opts.noteId);
const noteRecord = await this.#inner.getInputNote(noteHex);
if (!noteRecord) {
throw new Error(`Note not found: ${opts.noteId}`);
throw new Error(`Note not found: ${noteHex}`);
}
const note = noteRecord.toNote();
const address = resolveAddress(opts.to, wasm);
Expand All @@ -78,7 +84,9 @@ function buildNoteFilter(query, wasm) {
}

if (query.ids) {
const noteIds = query.ids.map((id) => wasm.NoteId.fromHex(id));
const noteIds = query.ids.map((id) =>
wasm.NoteId.fromHex(resolveNoteIdHex(id))
);
return new wasm.NoteFilter(wasm.NoteFilterTypes.List, noteIds);
}

Expand Down
Loading