# Gateway Addresses (GW) in Lethean

## 1. What are Gateway Addresses?

A Gateway Address (GW) is a new type of address in the Lethean blockchain that will be introduced starting with Hard Fork 6. Unlike classic addresses, which operate under the UTXO model, GW addresses use an **account-based model**, meaning that the balance is stored directly on the blockchain rather than being represented as a set of separate UTXOs.

GW addresses are designed to simplify integration with external systems such as cross-chain bridges, DEXs, exchanges, and payment gateways. They follow an approach that is more familiar to these services and provide a simple API that is, in some respects, closer to those used by more traditional blockchain platforms such as Bitcoin and Ethereum. This makes it easier for such services to use established custody frameworks, including MPC, and helps make the integration process more transparent and reliable.

In addition, because GW addresses use an **account-based model**, they avoid the issues related to UTXO fragmentation and distribution that are common in traditional UTXO-based blockchains.

## 2. Structure

Gateway Addresses are entities that must first be registered on the blockchain before they can be used. A GW address can only be registered through a standard Lethean wallet by calling the Wallet RPC API(see doc below). Once a GW address is registered, it is assigned an ID, which is effectively its public view key, and presented to user as a string starting from gwZ... (regular) or gwiZ...(integrated). After that, coins can be sent to this GW address. 

Each Gateway Address is associated with two private keys controlled by its owner: a **view key** and a **spend key(owner key)**. In the simplest case, the view key and the spend key may be the same.

In a more advanced setup, a GW address may use different view and spend keys. Importantly, the spend key can be replaced by the owner of the GW address.

### Keys associated with GW address

Each GW address is associated with **two** public keys: a **view key** and a **spend key**.

The **view key** is used exclusively to derive a shared secret between the sender and the recipient, and to encrypt additional information attached to the transaction, such as comments or a `payment_id`, using that secret. Once a view key has been associated with a GW address, it cannot be changed.

The **spend key** acts as a master key that controls all operations related to a GW address, including spending funds and assigning a new owner by replacing the spend key. From a security perspective, this is the most critical key, as it ultimately controls all funds associated with the address.

To make integration with Lethean convenient for a wide range of services, we implemented support for several signature types that are widely used across the blockchain industry. Below is a description of these signature types, along with the names of the corresponding API fields in the Wallet RPC API [register_gateway_address](https://testnet-docs.lthn.io/docs/build/rpc-api/wallet-rpc-api/register_gateway_address):

| Name in API | Curve | Public key | Signature | Use case |
|---|---|---|---|---|
| `opt_owner_ecdsa_pub_key` | secp256k1 | 33 bytes (compressed) | 64 bytes <br />(r \|\| s) | **ECDSA over secp256k1**. This signature type is widely used in blockchain projects such as Ethereum, Bitcoin, and others. |
| `opt_owner_eddsa_pub_key` | Ed25519 | 32 bytes | 64 bytes <br />(R \|\| s) |  **EDDSA** (also referred to as EdDSA). This is the variant used in Solana. |
| `opt_owner_custom_schnorr_pub_key` | Ed25519  | 32 bytes | 64 bytes <br />(c \|\| y) | **Lethean custom Schnorr signature**, also based on Ed25519. | 

**opt_owner_ecdsa_pub_key(ECDSA)** and **opt_owner_eddsa_pub_key(Ed25519)** were implemented primarily because these standards are widely supported across the blockchain industry and because there is extensive tooling available for building MPC solutions with these key types.

**opt_owner_custom_schnorr_pub_key(Lethean custom Schnorr signature)** is an internal algorithm native to the Lethean codebase and integrated into Lethean’s core transaction protocols. It has similarities to the scheme used in Solana and relies on the same elliptic curve, but for historical reasons it differs in several implementation details, including the hash function used in the Schnorr algorithm.

Note: View key (`view_pub_key`) can be only **Lethean custom Schnorr signature**, as it involved in internal protocol machinery. Only spend key could be assigned as **ECDSA**/**EDDSA***

All three types use the compact signature format (64 bytes, without the recovery byte `v`). The signature is transmitted as a hex string (128 characters).

### Privacy

When a transaction is sent to or from a GW address, some confidentiality is intentionally sacrificed for the parts of the transaction that directly involve the GW address, whether as the sender or the recipient:

- **Amounts** transferred to or from a GW address are stored in an open form. Anyone can see how much was sent or received by the GW address, while the counterparty remains hidden through commitments and stealth addresses.
- The **GW address itself** is visible in each input or output associated with it, whereas regular addresses remain hidden.
- The **Payment ID** always remains **encrypted** using key derivation from the view key. Decryption requires the `view_secret_key`.

This design makes GW addresses suitable for use cases such as bridges, exchanges, and similar services, where a certain level of transparency is acceptable or required.

---

## 3. Creating a GW address

Registration of a GW address is done via **wallet RPC** (`register_gateway_address`), a Lethean wallet with sufficient balance is required.

### Prerequisites
#### **IMPORTANT**: You must use **YOUR OWN NODE**, as the `view key` will be transferred there.
- Running and synced Lethean daemon (`lethean-chain-node`) on a network with hard fork 6+
- A wallet with RPC server enabled [(HOWTO)](https://testnet-docs.lthn.io/docs/build/exchange-guidelines/starting-the-daemon-and-the-wallet-application-as-rpc-server)
- Balance of at least **~100.01 LTHN** (100 LTHN - Registration fee + Default fee)

### Steps

#### Step 1: Key generation

Generate two key pairs:

1. **View keys** (`view_pub_key`, `view_secret_key`) - the GW address identifier. `view_pub_key` will become the address (`gwZ...`), and `view_secret_key` will be needed later to decrypt payment ID in transaction history
2. **Owner keys** (`owner_pub_key`, `owner_secret_key`) - the owners key for signing transactions. 

**Note:** In the simplest setup, the same key can be used both as the **view key** and as the **ownership key** by providing it as both `view_pub_key` and `opt_owner_custom_schnorr_pub_key` in `register_gateway_address`.  
However, to provide more complete guidance, the examples below use a more advanced configuration in which the owner and view keys would be different.


**Note on view key generating: L and main subgroup**

The GW address view key is a point on the Ed25519 curve. The Ed25519 curve has order `8 * L`, where `L` is the order of the prime-order subgroup:

[Wiki Curve25519 L magic number](https://en.wikipedia.org/wiki/Curve25519)
```
L = 2^252 + 27742317777372353535851937790883648493 
```

If the secret scalar is chosen arbitrarily (without the restriction `< L`), the resulting point may contain a **torsion component** — a small multiplier of order 2, 4, or 8. Such points lie outside the main subgroup and create a vulnerability: two different scalars can generate the same point (address collision).

During registration, Lethean Core verifies that `view_pub_key` belongs to the main L-subgroup (no torsion). Therefore, when generating a view key, you need to:

1. Select a random scalar `s` in the range `[1, L-1]`
2. Compute the public key as `s * G` 

This ensures that the public key resides in the main subgroup and that the registration will pass validation.

**ECDSA low-S normalisation**

[EIP-2 explaining link](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2.md)

For ECDSA signatures (secp256k1), Lethean requires that the value of `s` be in the "lower half" (`s <= n/2`, where `n` is the order of the secp256k1 curve). This is a standard requirement (EIP-2) that prevents signature malleability. Our `ethers.js` v6 library automatically normalizes `s` when calling `signingKey.sign()`, so no additional action is required.


**nodejs - keygen example**

```javascript
// gen Ed25519 view keys (valid scalar < L, main-subgroup public key)
const ED25519_L = (1n << 252n) + 27742317777372353535851937790883648493n;

function randomScalarLtL() {
  while (true) {
    const b = ethers.randomBytes(32);
    const x = BigInt('0x' + Buffer.from(b).toString('hex'));
    const s = x % ED25519_L;
    if (s !== 0n) return s;
  }
}

function scalarToLe32Hex(s) {
  const out = Buffer.alloc(32, 0);
  let x = s;
  for (let i = 0; i < 32; i++) {
    out[i] = Number(x & 0xffn);
    x >>= 8n;
  }
  return out.toString('hex');
}

const scalar = randomScalarLtL();
const viewSecretKey = scalarToLe32Hex(scalar);
const viewPubKey = Buffer.from(ed.Point.BASE.multiply(scalar).toBytes()).toString('hex');

console.log('View public key (GW address ID):', viewPubKey);
console.log('View secret key (save securely!!):', viewSecretKey);

// gen Ethereum owner keys for tx sign
const ownerWallet = ethers.Wallet.createRandom();
const ownerEthPubKey = ethers.SigningKey.computePublicKey(ownerWallet.privateKey, true).substring(2); // remove '0x', 33 bytes hex

console.log('Owner ETH public key:', ownerEthPubKey);
console.log('Owner ETH private key (save securely!):', ownerWallet.privateKey);
```

#### Step 2: Call register_gateway_address

```
User                  Wallet RPC            Blockchain
     |                     |                    |
     | 1. Generate keys    |                    |
     |    view + owner     |                    |
     |                     |                    |
     | register_gateway_   |                    |
     | address ----------> |                    |
     |                     | TX (fee=100 LTHN)  |
     |                     | -----------------> |
     |                     |                    | Validation:
     |                     |                    | - fee >= 100 LTHN
     |                     |                    | - no torsion
     |                     |                    | - address not taken
     |                     |                    |
     | gwZ..., tx_id  <--- |                    |
     |                     |                    |
```

**Request** (wallet JSON-RPC):

```json
{
  "jsonrpc": "2.0",
  "id": 0,
  "method": "register_gateway_address",
  "params": {
    "view_pub_key": "4dbaa579daf3c4a91e6be2efde9568975f7b506b50d18bbefd6f03132b2ef180",
    "descriptor_info": {
      "opt_owner_ecdsa_pub_key": "0375d5e222b50cb55ede0a70ebb398ebc9e5d5e74ea0cbce860d4a38301877f4f7",
      "meta_info": "Example GW address for documentation"
    }
  }
}
```

Instead of `opt_owner_ecdsa_pub_key` you can specify:
- `opt_owner_custom_schnorr_pub_key` - for Schnorr key (64 hex)
- `opt_owner_eddsa_pub_key` - for EdDSA key (64 hex)

Exactly **one** owner key type must be specified.

**Response:**

```json
{
  "jsonrpc": "2.0",
  "id": 0,
  "result": {
    "address": "gwZ5sqxb53vdd7RjUoPoSWeELimisUGprEX3P8fhcQwc8Dw6xpnSgfC1v",
    "address_id": "4dbaa579daf3c4a91e6be2efde9568975f7b506b50d18bbefd6f03132b2ef180",
    "status": "OK",
    "tx_id": "3a0af1fc8dd142234e0ee300ccd930964d1a17ff9bab51cf610818db0e82fc4a"
  }
}
```

After the transaction is confirmed in a block, the GW address is registered **permanently**.

**Node.js - registration and verification:**

```javascript
async function registerGatewayAddress(viewPubKey, ownerEthPubKey) {
  const regResult = await callWalletRpc('register_gateway_address', {
    view_pub_key: viewPubKey,
    descriptor_info: {
      opt_owner_ecdsa_pub_key: ownerEthPubKey,
      meta_info: 'Example GW address for documentation',
    },
  });

  console.log('Registered GW address:', regResult.address);
  console.log('Transaction ID:', regResult.tx_id);

  console.log('Waiting for confirmation...');
  await sleep(20000);

  const info = await callDaemonRpc('gateway_get_address_info', {
    gateway_address: regResult.address,
  });

  console.log('GW address info:', JSON.stringify(info, null, 2));
  console.log('Balances:', info.balances);
  console.log('View pub key:', info.gateway_view_pub_key);

  return regResult.address;
}
```

#### Step 3: Verify registration

Use daemon RPC `gateway_get_address_info`:

```json
{
  "jsonrpc": "2.0",
  "id": 0,
  "method": "gateway_get_address_info",
  "params": {
    "gateway_address": "gwZ5sqxb53vdd7RjUoPoSWeELimisUGprEX3P8fhcQwc8Dw6xpnSgfC1v"
  }
}
```

**Response:**

```json
{
  "jsonrpc": "2.0",
  "id": 0,
  "result": {
    "status": "OK",
    "descriptor_info": {
      "opt_owner_ecdsa_pub_key": "0375d5e222b50cb55ede0a70ebb398ebc9e5d5e74ea0cbce860d4a38301877f4f7",
      "meta_info": "Example GW address for documentation"
    },
    "balances": [],
    "gateway_view_pub_key": "4dbaa579daf3c4a91e6be2efde9568975f7b506b50d18bbefd6f03132b2ef180",
    "payment_id": ""
  }
}
```

Balances are still empty - the address was just created

---

## 4. Sending funds from a GW address (two-step API)

Sending funds from a GW address is done via **daemon RPC** (!not wallet RPC!) in three stages: creating an unsigned transaction, signing, and broadcasting to the network.

This approach allows signing transactions **outside the daemon** - for example, in MPC framework, on a cold wallet, or in any external system.

### General flow

```
External service            Daemon (lethean-chain-node)            Blockchain
      |                           |                       |
      |  gateway_create_          |                       |
      |  transfer --------------> |                       |
      |    origin_gateway_id      | Checks balance        |
      |    destinations, fee      | Builds unsigned TX    |
      |                           |                       |
      |  tx_hash_to_sign     <--- |                       |
      |  tx_blob                  |                       |
      |                           |                       |
      |  +--------------------+   |                       |
      |  | Signs               |  |                       |
      |  | tx_hash_to_sign     |  |                       |
      |  | with owner_key      |  |                       |
      |  | (offline/MetaMask)  |  |                       |
      |  +--------------------+   |                       |
      |                           |                       |
      |  gateway_sign_            |                       |
      |  transfer --------------> |                       |
      |    tx_blob + signature    | Inserts signature     |
      |                           |                       |
      |  signed_tx_blob      <--- |                       |
      |                           |                       |
      |  sendrawtransaction ----> | --------------------> |
      |                           |        Into block     |
```

### Step 1: Create transaction - `gateway_create_transfer`

**Request** (daemon JSON-RPC):

```json
{
  "jsonrpc": "2.0",
  "id": 0,
  "method": "gateway_create_transfer",
  "params": {
    "origin_gateway_id": "4dbaa579daf3c4a91e6be2efde9568975f7b506b50d18bbefd6f03132b2ef180",
    "destinations": [
      {
        "amount": 1000000000000,
        "address": "iTHNUNiuu3VP1yy8xH2y5iQaABKXurdjqZmzFiBiyR4dKG3j6534e9jMriY6SM7PH8NibVwVWW1DWJfQEWnSjS8n3Wgx86pQpY",
        "asset_id": "d6329b5b1f7c0805b5c345f4957554002a2f557845f64d7645dae0e051a6498a"
      }
    ],
    "fee": 100000000,
    "comment": "Payment for services"
  }
}
```

Destinations can be both regular addresses (`Z...`) and other GW addresses (`gwZ...`).

**Response:**

```json
{
  "jsonrpc": "2.0",
  "id": 0,
  "result": {
    "status": "OK",
    "tx_hash_to_sign": "20e922b32dfe9b8b6bc6004e40f4198c9e966d5e228cd4830656ba967f8a205c",
    "tx_blob": " "
  }
}
```

The daemon checks:
- The GW address exists in the blockchain
- Sufficient funds for each asset_id (including fee in native coin)

### Step 2: Sign transaction - `gateway_sign_transfer`

Take `tx_hash_to_sign` from the response and sign it with **your owner_secret_key** (the one whose public key was specified during registration). The signature can be generated in any external system.

**Request** (daemon JSON-RPC):

```json
{
  "jsonrpc": "2.0",
  "id": 0,
  "method": "gateway_sign_transfer",
  "params": {
    "tx_blob": " ",
    "tx_hash_to_sign": "20e922b32dfe9b8b6bc6004e40f4198c9e966d5e228cd4830656ba967f8a205c",
    "opt_ecdsa_signature": "c2b347b83da0a79637b74ad4b504030033b771ac8cb1f610757f82e88d112b1032ef615efb2cf3f2e70c15de9c63208ca80c1cea70f12785648c98a5ca3c7b40"
  }
}
```

Instead of `opt_ecdsa_signature` use:
- `opt_custom_schnorr_signature` - if owner key is Schnorr
- `opt_eddsa_signature` - if owner key is EdDSA

Exactly **one** signature must be specified.

**Response:**

```json
{
  "jsonrpc": "2.0",
  "id": 0,
  "result": {
    "status": "OK",
    "signed_tx_blob": " "
  }
}
```

### Step 3: Broadcast transaction - `sendrawtransaction`

**Request** (daemon JSON-RPC):

```json
{
  "jsonrpc": "2.0",
  "id": 0,
  "method": "sendrawtransaction",
  "params": {
    "tx_as_hex": " "
  }
}
```

After confirmation in a block, the GW address balance will be updated.

**Node.js - full send flow (create + sign + broadcast):**

```javascript
async function sendFromGateway(gatewayAddressId, recipientAddress, amount, ownerWallet) {
  const createResult = await callDaemonRpc('gateway_create_transfer', {
    origin_gateway_id: gatewayAddressId,
    destinations: [
      { amount: amount - 1, address: recipientAddress },
      { amount: 1, address: recipientAddress }
    ],
    fee: 10000000000,
    comment: 'Payment from GW address',
  });

  console.log('TX hash to sign:', createResult.tx_hash_to_sign);

  // sign with ETH private key
  const bytesToSign = ethers.getBytes('0x' + createResult.tx_hash_to_sign);
  const signature = ownerWallet.signingKey.sign(bytesToSign);
  const ethSignature = signature.r.slice(2) + signature.s.slice(2);

  console.log('ETH signature (r||s):', ethSignature);

  const signResult = await callDaemonRpc('gateway_sign_transfer', {
    tx_blob: createResult.tx_blob,
    tx_hash_to_sign: createResult.tx_hash_to_sign,
    opt_ecdsa_signature: ethSignature,
  });

  console.log('Transaction signed successfully');

  const sendResult = await callDaemonRpc('sendrawtransaction', {
    tx_as_hex: signResult.signed_tx_blob,
  });

  console.log('Transaction broadcasted:', sendResult.status);
  return createResult.tx_hash_to_sign;
}
```

### Sending TO a GW address

To send funds **to** a GW address, use the regular wallet RPC `transfer` - just specify `gwZ...` as the destination address:

```json
{
  "jsonrpc": "2.0",
  "id": 0,
  "method": "transfer",
  "params": {
    "destinations": [
      {
        "amount": 2000000000000,
        "address": "gwZ5sqxb53vdd7RjUoPoSWeELimisUGprEX3P8fhcQwc8Dw6xpnSgfC1v"
      }
    ],
    "fee": 100000000
  }
}
```

**Node.js - sending to a GW address:**

```javascript
async function sendToGateway(gwAddress, amount) {
  const result = await callWalletRpc('transfer', {
    destinations: [
      {
        amount: amount,
        address: gwAddress,
      },
    ],
    fee: 100000000,
  });

  console.log('Sent to GW address, tx_id:', result.tx_hash);
  return result.tx_hash;
}
```

---

## 5. Retrieving transaction history

Transaction history for a GW address is queried via **daemon RPC** using the `gateway_get_address_history` method.

### IMPORTANT: you need your own daemon

The `gateway_view_secret_key` parameter is passed to the daemon to decrypt payment IDs and attachments (comments). **If you use a public daemon**, this key will be exposed to its operator. For full security, **run your own node**.



### Request

```json
{
  "jsonrpc": "2.0",
  "id": 0,
  "method": "gateway_get_address_history",
  "params": {
    "gateway_address": "gwZ5sqxb53vdd7RjUoPoSWeELimisUGprEX3P8fhcQwc8Dw6xpnSgfC1v",
    "gateway_view_secret_key": "f74bb56a...view secret key (64 hex, optional)",
    "offset": 0,
    "count": 50
  }
}
```

Parameters:
- `gateway_address` - address `gwZ...` or `gwiZ...`
- `gateway_view_secret_key` - secret view key (optional; without it payment_id won't be decrypted)
- `offset` - pagination offset (starting from 0)
- `count` - number of transactions (maximum 200 per request)

### Response

```json
{
  "jsonrpc": "2.0",
  "id": 0,
  "result": {
    "status": "OK",
    "total_transactions": 2,
    "transactions": [
      {
        "tx_hash": "a85942c54afa26bb19ff77201d770d88956975e5c1c9ab72733ed17334122d6a",
        "height": 36322,
        "subtransfers_by_pid": [
          {
            "payment_id": "",
            "subtransfers": [
              {
                "asset_id": "d6329b5b1f7c0805b5c345f4957554002a2f557845f64d7645dae0e051a6498a",
                "is_income": true,
                "amount": 2000000000000
              }
            ]
          }
        ]
      },
      {
        "tx_hash": "20e922b32dfe9b8b6bc6004e40f4198c9e966d5e228cd4830656ba967f8a205c",
        "height": 36323,
        "subtransfers_by_pid": [
          {
            "payment_id": "",
            "subtransfers": [
              {
                "asset_id": "d6329b5b1f7c0805b5c345f4957554002a2f557845f64d7645dae0e051a6498a",
                "is_income": false,
                "amount": 1010000000000
              }
            ]
          }
        ],
        "comment": "Test transfer from GW"
      }
    ],
    "balances": [
      {
        "asset_id": "d6329b5b1f7c0805b5c345f4957554002a2f557845f64d7645dae0e051a6498a",
        "amount": 990000000000
      }
    ]
  }
}
```

Transactions are grouped by `payment_id`, and within each group - by `asset_id`. The `balances` field contains the current balances of the GW address at the time of the request.

**Node.js - fetching history:**

```javascript
async function getGatewayHistory(gwAddress, viewSecretKey, offset = 0, count = 50) {
  const result = await callDaemonRpc('gateway_get_address_history', {
    gateway_address: gwAddress,
    gateway_view_secret_key: viewSecretKey,  // pass to decrypt payment IDs
    offset: offset,
    count: count,
  });

  console.log('Total transactions:', result.total_transactions);
  console.log('Current balances:', JSON.stringify(result.balances, null, 2));

  for (const tx of result.transactions) {
    console.log(`\nTX: ${tx.tx_hash} (block ${tx.height})`);
    if (tx.comment) console.log(`  Comment: ${tx.comment}`);

    for (const pidGroup of tx.subtransfers_by_pid) {
      console.log(`  Payment ID: ${pidGroup.payment_id || '(none)'}`);
      for (const sub of pidGroup.subtransfers) {
        console.log(`    Asset ${sub.asset_id}: ${sub.is_income ? '+' : '-'}${sub.amount}`);
      }
    }
  }

  return result;
}
```

### How it works internally

1. The blockchain maintains an index: for each GW address, a list of related transaction hashes is stored
2. When a transaction is added to a block, all `txin_gateway` and `tx_out_gateway` referencing this GW address are automatically indexed
3. When history is requested, the daemon retrieves transactions by index and for each one determines:
   - Incoming or outgoing transaction (by the presence of `txin_gateway` / `tx_out_gateway`)
   - Decrypts payment ID (if `view_secret_key` is provided)
   - Calculates balance changes per payment_id and asset_id

---

## 6. Integrated GW addresses (payment_id)

GW addresses support **integrated addresses** with embedded payment IDs, similar to regular Lethean addresses.

- Integrated GW addresses start with `gwiZ...`
- Payment ID: 8 bytes (16 hex characters)
- Created via daemon RPC `get_integrated_address`:

```json
{
  "jsonrpc": "2.0",
  "id": 0,
  "method": "get_integrated_address",
  "params": {
    "regular_address": "gwZ5sqxb53vdd7RjUoPoSWeELimisUGprEX3P8fhcQwc8Dw6xpnSgfC1v",
    "payment_id": "1dfe5a88ff9effb3"
  }
}
```

**Response:**

```json
{
  "jsonrpc": "2.0",
  "id": 0,
  "result": {
    "integrated_address": "gwiZ92c1bNYdd7RjUoPoSWeELimisUGprEX3P8fhcQwc8Dw6xpk6AP3Pv4KbyNZJdF3y",
    "payment_id": "1dfe5a88ff9effb3"
  }
}
```

When sending funds to a `gwiZ...` address, the payment ID is automatically encrypted and included in the transaction. To decrypt it when retrieving history, the `view_secret_key` is needed.

**Node.js - creating an integrated address:**

```javascript
async function createIntegratedAddress(gwAddress, paymentId) {
  const result = await callDaemonRpc('get_integrated_address', {
    regular_address: gwAddress,
    payment_id: paymentId,
  });

  console.log('Integrated address:', result.integrated_address);
  console.log('Payment ID:', result.payment_id);
  return result.integrated_address;
}
```

---

## 7. Complete node.js example (end-to-end)

Below is a complete script that demonstrates the entire GW address lifecycle: key generation, registration, funding, transferring from GW, and reading history.

**Prerequisites:**
- `lethean-chain-node` running and synced (port 36941)
- `lethean-wallet-cli` running with RPC enabled (`--rpc-bind-port=11112`)
- Wallet has at least ~110 LTHN for registration fee + test transfers

```javascript
// npm i axios ethers @noble/ed25519
const axios = require('axios');
const { ethers } = require('ethers');
const ed = require('@noble/ed25519');

const WALLET_RPC_URL = 'http://127.0.0.1:11112/json_rpc';
const DAEMON_RPC_URL = 'http://127.0.0.1:36941/json_rpc';

const DEFAULT_FEE = 10_000_000_000;

// ed25519 subgroup order L
const ED25519_L = (1n << 252n) + 27742317777372353535851937790883648493n;

async function callWalletRpc(method, params = {}) {
  const response = await axios.post(WALLET_RPC_URL, {
    jsonrpc: '2.0', id: 0, method, params,
  });
  if (response.data.error) {
    throw new Error(`Wallet RPC [${method}]: ${JSON.stringify(response.data.error)}`);
  }
  return response.data.result;
}

async function callDaemonRpc(method, params = {}) {
  const response = await axios.post(DAEMON_RPC_URL, {
    jsonrpc: '2.0', id: 0, method, params,
  });
  if (response.data.error) {
    throw new Error(`Daemon RPC [${method}]: ${JSON.stringify(response.data.error)}`);
  }
  return response.data.result;
}

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function assertOk(result, methodName) {
  if (result.status && result.status !== 'OK') {
    throw new Error(`${methodName} returned status: ${result.status}`);
  }
}

async function waitForNextBlock(timeoutMs = 1800000) {
  const startInfo = await callDaemonRpc('getinfo');
  const startHeight = startInfo.height;
  console.log(`  current height: ${startHeight}, waiting for next block...`);
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    await sleep(5000);
    const info = await callDaemonRpc('getinfo');
    if (info.height > startHeight) {
      console.log(`  new height: ${info.height}`);
      return info.height;
    }
  }
  throw new Error(`Timeout waiting for next block (stuck at height ${startHeight})`);
}

function randomScalarLtL() {
  while (true) {
    const b = ethers.randomBytes(32);
    const x = BigInt('0x' + Buffer.from(b).toString('hex'));
    const s = x % ED25519_L;
    if (s !== 0n) return s;
  }
}

function scalarToLe32Hex(s) {
  const out = Buffer.alloc(32, 0);
  let x = s;
  for (let i = 0; i < 32; i++) {
    out[i] = Number(x & 0xffn);
    x >>= 8n;
  }
  return out.toString('hex');
}

function makeValidGwViewKeypair() {
  const scalar = randomScalarLtL();
  const viewSecretKey = scalarToLe32Hex(scalar);
  const viewPubKey = Buffer.from(ed.Point.BASE.multiply(scalar).toBytes()).toString('hex');
  return { viewSecretKey, viewPubKey };
}

async function main() {
  try {
    console.log('=== Key Generation ===');

    const { viewSecretKey, viewPubKey } = makeValidGwViewKeypair();
    const ownerWallet = ethers.Wallet.createRandom();
    const ownerEthPubKey = ethers.SigningKey.computePublicKey(ownerWallet.privateKey, true).substring(2);

    console.log('View public key:', viewPubKey);
    console.log('View secret key:', viewSecretKey);
    console.log('Owner ETH private key:', ownerWallet.privateKey);
    console.log('Owner ETH public key:', ownerEthPubKey);

    console.log('\n=== Registration (fee 100 LTHN) ===');

    const regResult = await callWalletRpc('register_gateway_address', {
      view_pub_key: viewPubKey,
      descriptor_info: {
        opt_owner_ecdsa_pub_key: ownerEthPubKey,
        meta_info: 'Example GW address for documentation',
      },
    });

    const gwAddress = regResult.address;
    const gwAddressId = regResult.address_id;
    console.log('GW address:', gwAddress);
    console.log('Address ID:', gwAddressId);
    console.log('Registration TX:', regResult.tx_id);

    console.log('\nWaiting for block confirmation...');
    await waitForNextBlock();

    const addressInfo = await callDaemonRpc('gateway_get_address_info', {
      gateway_address: gwAddress,
    });
    assertOk(addressInfo, 'gateway_get_address_info');
    console.log('Registration confirmed!');
    console.log('Descriptor:', JSON.stringify(addressInfo.descriptor_info, null, 2));
    console.log('Balances:', JSON.stringify(addressInfo.balances));

    console.log('\n=== Sending 2 LTHN to GW address ===');

    const sendToResult = await callWalletRpc('transfer', {
      destinations: [{ amount: 2_000_000_000_000, address: gwAddress }],
      fee: DEFAULT_FEE,
    });
    console.log('Sent to GW, tx_hash:', sendToResult.tx_hash);

    console.log('Waiting for block confirmation...');
    await waitForNextBlock();

    const updatedInfo = await callDaemonRpc('gateway_get_address_info', {
      gateway_address: gwAddress,
    });
    assertOk(updatedInfo, 'gateway_get_address_info');
    console.log('GW balances after funding:', JSON.stringify(updatedInfo.balances, null, 2));

    console.log('\n=== Sending 1 LTHN from GW address ===');

    const walletInfo = await callWalletRpc('getaddress');
    const recipientAddress = walletInfo.address;
    console.log('Recipient (own wallet):', recipientAddress);

    const totalAmount = 1_000_000_000_000;
    const createResult = await callDaemonRpc('gateway_create_transfer', {
      origin_gateway_id: gwAddressId,
      destinations: [
        { amount: totalAmount - 1, address: recipientAddress },
        { amount: 1, address: recipientAddress },
      ],
      fee: DEFAULT_FEE,
      comment: 'Test transfer from GW',
    });
    assertOk(createResult, 'gateway_create_transfer');
    console.log('TX hash to sign:', createResult.tx_hash_to_sign);

    // sign with ETH private key (ECDSA secp256k1, low-s normalized)
    const bytesToSign = ethers.getBytes('0x' + createResult.tx_hash_to_sign);
    const sig = ownerWallet.signingKey.sign(bytesToSign);
    const ethSignature = sig.r.slice(2) + sig.s.slice(2); // r||s, 128 hex = 64 bytes
    console.log('Signature (128 hex):', ethSignature);

    const signResult = await callDaemonRpc('gateway_sign_transfer', {
      tx_blob: createResult.tx_blob,
      tx_hash_to_sign: createResult.tx_hash_to_sign,
      opt_ecdsa_signature: ethSignature,
    });
    assertOk(signResult, 'gateway_sign_transfer');
    console.log('Transaction signed, blob length:', signResult.signed_tx_blob.length);

    const broadcastResult = await callDaemonRpc('sendrawtransaction', {
      tx_as_hex: signResult.signed_tx_blob,
    });
    if (broadcastResult.status !== 'OK') {
      throw new Error(`Broadcast failed: ${broadcastResult.status}`);
    }
    console.log('Broadcast OK!');

    console.log('Waiting for block confirmation...');
    await waitForNextBlock();

    console.log('\n=== Creating integrated GW address ===');

    const integratedResult = await callDaemonRpc('get_integrated_address', {
      regular_address: gwAddress,
      payment_id: '1dfe5a88ff9effb3',
    });
    console.log('Integrated address:', integratedResult.integrated_address);
    console.log('Payment ID:', integratedResult.payment_id);

    console.log('\n=== Fetching transaction history ===');

    const history = await callDaemonRpc('gateway_get_address_history', {
      gateway_address: gwAddress,
      gateway_view_secret_key: viewSecretKey,
      offset: 0,
      count: 50,
    });

    console.log('Total transactions:', history.total_transactions);
    console.log('Current balances:', JSON.stringify(history.balances, null, 2));

    for (const tx of history.transactions) {
      console.log(`\nTX: ${tx.tx_hash} (block ${tx.height})`);
      if (tx.comment) console.log(`  Comment: ${tx.comment}`);
      for (const pidGroup of tx.subtransfers_by_pid) {
        console.log(`  Payment ID: ${pidGroup.payment_id || '(none)'}`);
        for (const sub of pidGroup.subtransfers) {
          console.log(`    Asset ${sub.asset_id}: ${sub.is_income ? '+' : '-'}${sub.amount}`);
        }
      }
    }

    console.log('\nDone!');
  } catch (error) {
    console.error('\nERROR:', error.message);
    if (error.response) {
      console.error('Response data:', JSON.stringify(error.response.data, null, 2));
    }
  }
}

main();
```

---

## API quick reference

| Method | RPC type | Description |
|---|---|---|
| [register_gateway_address](https://testnet-docs.lthn.io/docs/build/rpc-api/wallet-rpc-api/register_gateway_address) | Wallet RPC | Register a new GW address (fee: 100 LTHN) |
| [gateway_get_address_info](https://testnet-docs.lthn.io/docs/build/rpc-api/daemon-rpc-api/gateway_get_address_info) | Daemon RPC | Get information and balances of a GW address |
| [gateway_create_transfer](https://testnet-docs.lthn.io/docs/build/rpc-api/daemon-rpc-api/gateway_create_transfer) | Daemon RPC | Create an unsigned transaction from a GW address |
| [gateway_sign_transfer](https://testnet-docs.lthn.io/docs/build/rpc-api/daemon-rpc-api/gateway_sign_transfer) | Daemon RPC | Sign a transaction with owner key |
| [sendrawtransaction](https://testnet-docs.lthn.io/docs/build/rpc-api/daemon-rpc-api/sendrawtransaction) | Daemon RPC | Broadcast a signed transaction to the network |
| [gateway_get_address_history](https://testnet-docs.lthn.io/docs/build/rpc-api/daemon-rpc-api/gateway_get_address_history) | Daemon RPC | Get GW address transaction history (requires view key for decryption) |
| [get_integrated_address](https://testnet-docs.lthn.io/docs/build/rpc-api/daemon-rpc-api/get_integrated_address) | Daemon RPC | Create an integrated `gwiZ...` address with payment ID |
| [transfer](https://testnet-docs.lthn.io/docs/build/rpc-api/wallet-rpc-api/transfer) | Wallet RPC | Send funds TO a GW address (standard method) |

