Using Signer JS Library (Low-Level TypeScript)

This library is suitable if you need more control over the integration process, are not using React, or want to build a custom UI. It directly implements the client side of the ICRC wallet standards.

Install: You'll need the core library, the web transport for browser-based wallets like OISY, and potentially the agent helper.

npm install @slide-computer/signer @slide-computer/signer-web @slide-computer/signer-agent @dfinity/agent @dfinity/principal @dfinity/ledger-icrc
# or yarn add ...
# or pnpm add ...
  • @slide-computer/signer: Core signer logic and interfaces.

  • @slide-computer/signer-web: Implements PostMessageTransport for ICRC-29 (communication with browser wallets).

  • @slide-computer/signer-agent: A utility to create an @dfinity/agent instance that uses the @slide-computer/signer.

  • @dfinity/ledger-icrc: Helpers for interacting with ICRC-1 ledgers (like the ICP ledger, though it's technically not ICRC-1 but similar for transfers).

Connect to OISY (Example): You need to trigger this connection process from a user action, like clicking a "Connect OISY" button.

import { Signer } from '@slide-computer/signer';
import { PostMessageTransport, type PostMessageTransportOptions } from '@slide-computer/signer-web';
import { SignerAgent } from '@slide-computer/signer-agent';
import { IcrcLedgerCanister, type Account } from '@dfinity/ledger-icrc'; // Using ICRC-1 types for accounts
import { Principal } from '@dfinity/principal';
import { Actor, HttpAgent } from '@dfinity/agent'; // Actor is needed, HttpAgent might be for other non-signed calls

// Ensure you have the IDL for the ICP ledger if not using a higher-level library for it
// For this example, we'll use IcrcLedgerCanister from @dfinity/ledger-icrc which has its own IDL.

let signerInstance: Signer | null = null;
let transportInstance: PostMessageTransport | null = null;
let walletAgent: SignerAgent | null = null; // This will be the agent that uses the wallet
let userPrincipal: Principal | null = null;
let userAccountsFromWallet: Account[] | null = null; // Using ICRC-1 Account type

// OISY's official signing endpoint
const OISY_SIGN_URL = 'https://oisy.com/sign';
const ICP_LEDGER_CANISTER_ID = 'ryjl3-tyaaa-aaaaa-aaaba-cai';

async function connectOisyWithSignerJs() {
  console.log('Attempting to connect OISY with signer-js...');

  // 1. Configure and create the transport for OISY (uses ICRC-29 postMessage)
  const transportOptions: PostMessageTransportOptions = {
    targetUrl: OISY_SIGN_URL,
    // Configure window features for the popup
    windowFeatures: 'width=500,height=700,noopener,noreferrer',
  };
  transportInstance = new PostMessageTransport(transportOptions);

  // 2. Create the Signer instance with the transport
  signerInstance = new Signer({ transport: transportInstance });

  try {
    // 3. Initiate the connection (this opens the OISY window for user approval)
    // This step handles the ICRC-25 handshake.
    await transportInstance.connect();
    console.log('OISY transport connected successfully!');

    // 4. Get user accounts (uses ICRC-27)
    // This retrieves the principal and associated accounts the user has permitted.
    const accountsResult = await signerInstance.accounts();
    if (!accountsResult || accountsResult.length === 0) {
      throw new Error('No accounts found or permission denied by user.');
    }
    userAccountsFromWallet = accountsResult.map((acc) => ({
      owner: acc.owner,
      subaccount: acc.subaccount,
    }));
    userPrincipal = userAccountsFromWallet.owner; // Typically use the principal from the first account
    console.log('OISY Accounts:', userAccountsFromWallet);
    console.log('User Principal:', userPrincipal.toText());

    // 5. Create a SignerAgent (optional but highly convenient)
    // This agent will use our 'signerInstance' to sign any outgoing update calls.
    // For query calls, it can use a standard anonymous HttpAgent or be configured.
    walletAgent = await SignerAgent.create({
      signer: signerInstance,
      identity: userPrincipal, // The identity obtained from the wallet
      host: 'https://icp-api.io', // Mainnet IC boundary node URL
    });
    console.log('SignerAgent created successfully.');

    // Update your UI to reflect the connected state
    updateUIOnConnect(userPrincipal.toText(), userAccountsFromWallet); // Implement this function
  } catch (error) {
    console.error('OISY Connection failed:', error);
    alert(`OISY Connection Error: ${error.message || error}`);
    await disconnectOisyWithSignerJs(); // Clean up any partial connection
  }
}

async function disconnectOisyWithSignerJs() {
  if (transportInstance && transportInstance.connected) {
    await transportInstance.disconnect();
  }
  signerInstance = null;
  transportInstance = null;
  walletAgent = null;
  userPrincipal = null;
  userAccountsFromWallet = null;
  updateUIOnDisconnect(); // Implement this function
  console.log('Disconnected from OISY.');
}

// --- Placeholder UI update functions (you need to implement these) ---
function updateUIOnConnect(principalText: string, account: Account) {
  document.getElementById('connect-oisy-signerjs').style.display = 'none';
  document.getElementById('disconnect-oisy-signerjs').style.display = 'block';
  document.getElementById('user-info').innerText =
    `Connected: ${principalText}, Account: ${JSON.stringify(account)}`;
  document.getElementById('transfer-icp-signerjs').style.display = 'block';
}
function updateUIOnDisconnect() {
  document.getElementById('connect-oisy-signerjs').style.display = 'block';
  document.getElementById('disconnect-oisy-signerjs').style.display = 'none';
  document.getElementById('user-info').innerText = 'Not Connected';
  document.getElementById('transfer-icp-signerjs').style.display = 'none';
}

// --- Add button listeners in your HTML ---
// <button id="connect-oisy-signerjs">Connect OISY (signer-js)</button>
// <button id="disconnect-oisy-signerjs" style="display:none;">Disconnect</button>
// <p id="user-info">Not Connected</p>
// <button id="transfer-icp-signerjs" style="display:none;">Send ICP (signer-js)</button>
document
  .getElementById('connect-oisy-signerjs')
  ?.addEventListener('click', connectOisyWithSignerJs);
document
  .getElementById('disconnect-oisy-signerjs')
  ?.addEventListener('click', disconnectOisyWithSignerJs);

Making Calls (Example with SignerAgent): Use the walletAgent (which is a SignerAgent) to create actors and call canister methods.

// (Continuing from the previous signer-js example)

async function transferIcpWithSignerJs() {
  if (!walletAgent || !userAccountsFromWallet || userAccountsFromWallet.length === 0) {
    alert('Not connected or no accounts available!');
    return;
  }

  // Example recipient (ensure this is a valid account structure for the ledger)
  const recipientPrincipal = Principal.fromText('uzr34-vyaaa-aaaaq-aaaea-cai'); // Replace
  const recipientAccount: Account = { owner: recipientPrincipal, subaccount: [] }; // ICRC-1 style account
  const amount = 500_000n; // 0.005 ICP (500,000 e8s)

  try {
    // Use IcrcLedgerCanister helper for ICRC-1 compliant ledgers.
    // Note: The main ICP ledger is not strictly ICRC-1 but has a similar transfer method.
    // For the actual ICP ledger, you might need its specific IDL and AccountIdentifier type.
    // This example assumes an ICRC-1 ledger for simplicity with @dfinity/ledger-icrc.
    // If targeting ICP ledger, you'd use its specific `transfer` args.

    // Let's assume we are interacting with an ICRC-1 ledger canister
    const icrcLedger = IcrcLedgerCanister.create({
      agent: walletAgent, // The agent that will use OISY for signing
      canisterId: Principal.fromText('mxzaz-hqaaa-aaaar-qaada-cai'), // Example ICRC-1 ledger ID (ckBTC)
    });

    console.log('Requesting ICRC-1 transfer via signer-js agent to OISY...');
    // This call will be sent to OISY for signing via the SignerAgent -> Signer -> Transport chain.
    // OISY will show an ICRC-21 consent message.
    const blockIndex = await icrcLedger.transfer({
      to: recipientAccount,
      amount: amount,
      // fee, memo, from_subaccount, created_at_time are optional for ICRC-1 transfer
      // and might be automatically filled or handled by OISY/ledger defaults.
    });

    console.log(`ICRC-1 Transfer OK! Block index: ${blockIndex}`);
    alert(`ICRC-1 Transfer successful! Block index: ${blockIndex}`);
  } catch (error) {
    console.error('ICRC-1 Transfer failed:', error);
    alert(`ICRC-1 Transfer failed: ${error.message || error}`);
  }
}

// Add listener to a transfer button
document
  .getElementById('transfer-icp-signerjs')
  ?.addEventListener('click', transferIcpWithSignerJs);

When icrcLedger.transfer (or any other update call via an actor using walletAgent) is executed, the SignerAgent directs the signing request to OISY. OISY then presents the user with a consent popup (formatted according to ICRC-21, if the wallet supports it well). If the user approves, OISY signs the transaction, and it's dispatched to the IC.

Signer JS Summary

This approach involves more manual setup for UI and state management but offers high flexibility and direct use of ICRC standards. It's ideal for non-React projects, custom UIs, or when you need a deep, standards-compliant integration that could work with various wallets supporting ICRC-25/27/29/49.

Last updated