Withdrawing NEO or GAS

Since NEO and GAS are UTXO based assets that cannot be sent directly from a smart contract, withdrawing NEO or GAS is a two NEO transaction process.

  1. Creating a UTXO for the withdraw amount reserved for the withdraw address.
  2. Send the reserved DEX UTXO to the withdraw address.

These transactions can be performed by directly by interacting with NEO RPC nodes and do not require any interaction with Aphelion servers. However, in order to simplify obtaining available UTXOs from the contract, a call can be made to aphelion's RPC nodes that include a plugin that returns unspent NEO and GAS UTXOs by the contract that are not already marked for withdraw in order to help avoid withdraw UTXO contention.

Procedure:

  1. Make a call to obtain UTXOs adding to the balance desired to be withdrawn.
  2. Invoke the contract withdraw operation to mark the transaction. This sends contract UTXO(s)
    back to the contract into 1 UTXO marked for withdraw.
  3. Invoke the contract withdraw operation to send the UTXO marked for withdraw back to the user wallet.
import {
  u,
  rpc,
  wallet,
} from '@cityofzion/neon-js';

const TX_ATTR_USAGE_SIGNATURE_REQUEST_TYPE = 0xA1;
const SIGNATUREREQUESTTYPE_WITHDRAWSTEP_MARK = '91';
const SIGNATUREREQUESTTYPE_WITHDRAWSTEP_WITHDRAW = '92';
const TX_ATTR_USAGE_WITHDRAW_SCRIPT_HASH = 0xA2;
const TX_ATTR_USAGE_WITHDRAW_SYSTEM_ASSET_ID = 0xA3;
const TX_ATTR_USAGE_WITHDRAW_AMOUNT = 0xA5;
const TX_ATTR_USAGE_WITHDRAW_VALIDUNTIL = 0xA6;
const DEX_HASH = '9488220e8654d798f9b9cb9e74bd611ecc83fd0f';
const NEO_ASSET_ID = 'c56f33fc6ecfcd0c225c4ab356fee59390af8560be0e930faebe74a6daff7c9b';


async function withdrawSystemAsset(accountScriptHash, systemAssetId, quantity, validUntilBlockIndex, gasFee=0) {
  const rpcClient = new rpc.RPCClient('https://mainneo.aphelion-neo.com:10331');

  // Step 1 - Acquire UTXO inputs to use for withdraw
  const res = await rpcClient.query({
      method: 'getunmarkedunspents',
      params: [
        DEX_HASH, 
        quantity,
        systemAssetId === NEO_ASSET_ID, // isNeo: set true for NEO, set false for GAS
        [] // Array of utxo pairs to ignore (useful to retry ignoring a particular UTXO)
      ],
    });
  let specificMarkInputs = [];
  
  if (res.result.balance.unspent.length <= 0) {
    throw new Error('No unspent outputs found to perform withdraw.');
  }
  
  let totalInput = 0;
  res.result.balance.unspent.forEach((unspent) => {
    specificMarkInputs.push({
      prevHash: unspent.txid,
      prevIndex: unspent.n,          
    });
    totalInput += unspent.value;
  });
  
  if (totalInput < quantity) {
    throw new Error('Not enough DEX inputs found to fulfill desired quantity. Try again.');
  }
  
  const specificOutputs = [];
  
  specificOutputs.push({
    systemAssetId,
    scriptHash: DEX_HASH,
    value: quantity,
  });
  
  if (quantity < totalInput) {
    specificOutputs.push({
      systemAssetId,
      scriptHash: DEX_HASH,
      value: quantity - totalInput,
    });
  }
  
  // Step 2 - Create UTXO Output
  // The DEX contract must be invoked with the `withdraw` operation in order to create an output 
  // UTXO to be withdrawn using the input DEX UTXOs acquired from step 1.
  const validUntilValue = validUntilBlockIndex * 0.00000001;
  const attribs = [
    [TX_ATTR_USAGE_SIGNATURE_REQUEST_TYPE, SIGNATUREREQUESTTYPE_WITHDRAWSTEP_MARK.padEnd(64, '0')],
    [TX_ATTR_USAGE_WITHDRAW_SCRIPT_HASH, u.reverseHex(accountScriptHash).padEnd(64, '0')],
    [TX_ATTR_USAGE_WITHDRAW_SYSTEM_ASSET_ID, u.reverseHex(systemAssetId).padEnd(64, '0')],
    [TX_ATTR_USAGE_WITHDRAW_AMOUNT, u.num2fixed8(quantity).padEnd(64, '0')],
    [TX_ATTR_USAGE_WITHDRAW_VALIDUNTIL, u.num2fixed8(validUntilValue).padEnd(64, '0')]
  ];
  const configMarkResp = executeContractTransaction('withdraw', [], 0, 0, gasFee, true, attribs, specificMarkInputs, specificOutputs);
  
  let i = 0;
  let utxoIndex = -1;
  configMarkResp.tx.outputs.forEach(({ value }) => {
    if (utxoIndex === -1 && quantity === value) {
      utxoIndex = i;
    }
    i += 1;
  });
  
  const specificWithdrawInputs = [];
  specificWithdrawInputs.push({
    prevHash: configMarkResp.tx.hash,
    prevIndex: utxoIndex,          
  });
  
  // Step 3 - Withdraw UTXO
  const validUntilValue2 = (validUntilBlockIndex+5) * 0.00000001;
    const attribs2 = [
      [TX_ATTR_USAGE_SIGNATURE_REQUEST_TYPE, SIGNATUREREQUESTTYPE_WITHDRAWSTEP_WITHDRAW.padEnd(64, '0')],
      [TX_ATTR_USAGE_WITHDRAW_SCRIPT_HASH, u.reverseHex(accountScriptHash).padEnd(64, '0')],
      [TX_ATTR_USAGE_WITHDRAW_SYSTEM_ASSET_ID, u.reverseHex(systemAssetId).padEnd(64, '0')],
      [TX_ATTR_USAGE_WITHDRAW_AMOUNT, u.num2fixed8(quantity).padEnd(64, '0')],
      [TX_ATTR_USAGE_WITHDRAW_VALIDUNTIL, u.num2fixed8(validUntilValue2).padEnd(64, '0')]
    ];
    executeContractTransaction('withdraw', [], 0, 0, gasFee, true, attribs2, specificWithdrawInputs);
}

const account = new wallet.Account(wif); 
const accountScriptHash = wallet.getScriptHashFromAddress(account.address);
// Example withdrawing 100 NEO.
await withdrawSystemAsset(accountScriptHash, NEO_ASSET_ID, 100);

// Example withdrawing 1.5 GAS.
await withdrawSystemAsset(accountScriptHash, '602c79718b16e442de58778e148d0b1084e3b2dffd5de6b7b16cee7969282de7', 1.5);