311 lines
9.3 KiB
Markdown
311 lines
9.3 KiB
Markdown
|
|
# 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:
|
||
|
|
|
||
|
|
```text
|
||
|
|
example-marketplace
|
||
|
|
```
|
||
|
|
|
||
|
|
## Installation
|
||
|
|
|
||
|
|
Download `contractless.js` with the application and import it as an ES module:
|
||
|
|
|
||
|
|
```js
|
||
|
|
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
|
||
|
|
|
||
|
|
```js
|
||
|
|
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:
|
||
|
|
|
||
|
|
```js
|
||
|
|
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:
|
||
|
|
|
||
|
|
```js
|
||
|
|
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:
|
||
|
|
|
||
|
|
```js
|
||
|
|
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:
|
||
|
|
|
||
|
|
```js
|
||
|
|
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
|
||
|
|
|
||
|
|
```js
|
||
|
|
const accounts = await wallet.accounts();
|
||
|
|
```
|
||
|
|
|
||
|
|
An empty array means the application no longer has a valid wallet session.
|
||
|
|
|
||
|
|
## Signing A Text Message
|
||
|
|
|
||
|
|
```js
|
||
|
|
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
|
||
|
|
|
||
|
|
```js
|
||
|
|
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:
|
||
|
|
|
||
|
|
```js
|
||
|
|
const result = await wallet.signDualTransaction({
|
||
|
|
transaction,
|
||
|
|
signerSlot: 1
|
||
|
|
});
|
||
|
|
|
||
|
|
await sendSignatureToApplication({
|
||
|
|
transactionId: result.txid,
|
||
|
|
result
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
The second party's browser independently runs:
|
||
|
|
|
||
|
|
```js
|
||
|
|
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:
|
||
|
|
|
||
|
|
```js
|
||
|
|
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:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
./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:
|
||
|
|
|
||
|
|
```js
|
||
|
|
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
|
||
|
|
|
||
|
|
```js
|
||
|
|
await wallet.disconnect();
|
||
|
|
```
|
||
|
|
|
||
|
|
Applications must never ask users to paste private keys, wallet files,
|
||
|
|
encryption keys, or access keys into a website.
|