|
|
||
|---|---|---|
| .. | ||
| README.md | ||
| contractless.js | ||
README.md
Contractless Browser Wallet JavaScript
contractless.js is the website client for the Contractless browser wallet.
It detects the injected wallet provider and manages a temporary,
origin-bound application session.
Application ID
Choose a readable application ID containing 5 to 32 letters, numbers, underscores, or hyphens, and keep it unchanged for the lifetime of the application. It only needs to distinguish applications using the same website origin. The wallet binds the ID to the website's actual browser origin, so the same ID used by a different website does not share access.
Example:
example-marketplace
Installation
Download contractless.js with the application and import it as an ES module:
import {
ContractlessWallet,
ContractlessWalletNotInstalledError
} from "./contractless.js";
const wallet = new ContractlessWallet({
appId: "example-marketplace",
api: {
url: "https://api.contractless.dev",
address: "dedicated-application-address.cltc",
publicKey: "dedicated-application-public-key",
signature: "precomputed-signature-for-aced",
apiKey: "optional-public-quota-key"
}
});
Detecting The Wallet
if (!wallet.isInstalled()) {
// Show the application's Contractless Wallet installation link.
}
The provider is injected at document start. Applications that execute
immediately can use waitForContractlessWallet() before showing an
installation notice.
Connecting
Use a server-generated nonce when the signed proof will authenticate a user with an application server:
const connection = await wallet.connect({
message: "Sign in to Example Marketplace",
timestamp: Math.floor(Date.now() / 1000),
nonce: "4f3c2a1b9d8e7f60514233445566778899aabbccddeeff001122334455667788"
});
console.log(connection.address);
console.log(connection.signature);
console.log(connection.proof);
After approval, the wallet generates a 256-bit access key. The library stores
it in the website tab's sessionStorage. The matching authorization remains
inside extension session storage.
Reading Balances
Applications can request every balance owned by the connected wallet:
const balances = await wallet.balances();
Use getBalance() when only one base coin, token, NFT, or RWA balance is
needed. Missing assets return a zero balance instead of an error:
const cltc = await wallet.getBalance({ asset: "CLTC" });
const nft = await wallet.getBalance({ asset: "FLOWERPRINCESS", nftSeries: 0 });
console.log(cltc.balance, cltc.balance_atomic);
Applications may use these methods to warn about an insufficient balance before requesting a transaction. The wallet still performs its own checks, and the node remains the final authority because balances can change between lookup and broadcast.
Transaction Errors
Rejected transaction Promises contain the useful verification reason returned by the node, such as an insufficient balance, an unregistered participant, or a fee below the required minimum. Applications should display the Error message to the user instead of replacing it with a generic failure notice.
The wallet checks directly named participants before showing transfer, swap, and loan approvals. This catches unregistered receivers and counterparties before the user is asked to sign.
The key is reusable for that wallet unlock session. It is not consumed after one request.
The session becomes invalid when:
- The Contractless wallet locks.
- The active wallet changes.
- The configured wallet lock period expires.
- The application calls
disconnect(). - The website tab's session storage is cleared.
The SDK removes the stored website session as soon as its expiration time is reached. Applications can update their interface when that happens:
window.addEventListener("contractless:sessionExpired", () => {
showWalletAsDisconnected();
});
If an application later requests an account, signature, or transaction without a usable session, the SDK automatically opens the wallet connection flow. A locked wallet asks the user to unlock it, creates a new session after approval, and then continues the original request. The website never reuses an expired access key.
Reading The Connected Address
const accounts = await wallet.accounts();
An empty array means the application no longer has a valid wallet session.
Signing A Text Message
const proof = await wallet.signMessage({
message: "Confirm profile ownership",
nonce: "1188aabbccddeeff00112233445566778899aabbccddeeff00112233445566"
});
Text signatures are domain-separated from transactions and cannot move wallet assets. Website connection and text-signature messages are limited to 75 UTF-8 bytes. The wallet calculates this limit from bytes rather than visible character count.
Sending A Transaction
const result = await wallet.sendTransaction({
transaction: {
txtype: 2,
time: Math.floor(Date.now() / 1000),
value: "2500000000",
coin: "CLTC ",
nft_series: 0,
sender: "sender-address.cltc",
receiver: "receiver-address.cltc",
txfee: "25000000"
}
});
console.log(result.txid);
console.log(result.bytes);
Applications must provide the complete transaction object. The wallet validates and serializes that object, calculates its hash locally, displays the signed fields, and signs only the hash it calculated. After approval, the wallet broadcasts the signed bytes through its configured Contractless API. The transaction ID and signed bytes are returned only after that API accepts the broadcast. Arbitrary hash signing is not exposed.
Swaps And Loan Creation
Swaps and loan creation use two independent wallet signatures. The application sends the exact same unsigned transaction to two separate computers. Neither party receives or validates the other party's signature before signing.
The first party's browser runs:
const result = await wallet.signDualTransaction({
transaction,
signerSlot: 1
});
await sendSignatureToApplication({
transactionId: result.txid,
result
});
The second party's browser independently runs:
const result = await wallet.signDualTransaction({
transaction,
signerSlot: 2
});
await sendSignatureToApplication({
transactionId: result.txid,
result
});
Each response contains the same transaction hash, canonical unsigned data,
unsigned wire-byte prefix, the signer's slot, public key, and that wallet's
signature. sendSignatureToApplication() represents an application endpoint,
database, matchmaking service, or other shared coordination layer. It is not a
wallet SDK function.
After the shared application has received both results, it returns them to one client for final verification, assembly, and broadcast:
const { transaction, signer1, signer2 } = await getCompletedSigningRequest(
transactionId
);
const complete = await wallet.completeDualTransaction({
transaction,
signer1,
signer2
});
const broadcast = await wallet.broadcastTransaction({
bytes: complete.bytes_hex
});
console.log(complete.hash);
console.log(complete.bytes_hex);
console.log(broadcast);
The final client does not sign again. It only invokes the browser core to compare the transaction data, verify both Falcon signatures, and construct the complete bytes. An application backend implementing the same Contractless codec may instead perform verification, assembly, and broadcasting itself.
For swaps, signer slot 1 is sender1 and signer slot 2 is sender2. For loan
creation, signer slot 1 is the lender and signer slot 2 is the borrower.
The application may store pending details locally, but it must never store a
wallet private key or encryption key.
completeDualTransaction() checks that both wallets signed the same hash,
that each public key belongs to the address assigned to its signer slot, and
that both Falcon signatures are valid. It does not ask either wallet to sign
again.
Direct API Identity
An application can use a dedicated, registered, zero-balance wallet identity for public API lookups and broadcasting already signed transactions. Generate its reusable handshake signature with the node CLI:
./sign_message "aced"
Configure that address, its public key, and the returned signature in the SDK constructor as shown above. This proof permits the API and node handshake; it does not permit the application to sign transactions or spend from the identity.
Applications can use the same identity for API lookups:
const balance = await wallet.apiRequest("/api/v1/balances/base", {
query: {
address: userAddress,
coin: "CLTC"
}
});
apiRequest() accepts any enabled /api/v1 route, optional query values, an
HTTP method, and an optional JSON body. It returns the API's data value and
throws when the API rejects the request.
These values are visible to anyone who can inspect the website. Use a dedicated address that holds no funds. An API key embedded in browser JavaScript is also public and should only identify traffic or select a quota. Services requiring a secret API key or HMAC secret must proxy API requests through a backend.
Contractless API and PHP integration source:
- https://contractless.dev/contractless/Contractless-PHP-API
- https://contractless.dev/contractless/Contractless-PHP-RPC
Disconnecting
await wallet.disconnect();
Applications must never ask users to paste private keys, wallet files, encryption keys, or access keys into a website.