PHP WebShell

Текущая директория: /opt/BitGoJS/modules/sdk-coin-sui/test/local_fullnode

Просмотр файла: transactions.ts

import { StakeObject } from '../../src/lib/mystenlab/types/validator';

process.env.DEBUG = 'RpcClient,SuiTransactionTypes';

import assert from 'assert';
import util from 'util';
import { Faucet } from './faucet';
import buildDebug from 'debug';
import { createHash } from 'crypto';

import {
  KeyPair,
  StakingBuilder,
  StakingTransaction,
  TransferBuilder,
  TransferTransaction,
  UnstakingBuilder,
  UnstakingTransaction,
} from '../../src';
import { RpcClient } from './RpcClient';
import { getBuilderFactory } from '../unit/getBuilderFactory';
import { SuiTransactionType } from '../../src/lib/iface';
import { GasData, SuiObjectRef } from '../../src/lib/mystenlab/types';
import { DUMMY_SUI_GAS_PRICE, MIN_STAKING_THRESHOLD } from '../../src/lib/constants';

const debug = buildDebug('SuiTransactionTypes');

async function getAllCoins(conn: RpcClient, address: string): Promise<SuiObjectRef[]> {
  return (await conn.getCoins(address)).data.map((v) => {
    return {
      digest: v.digest,
      objectId: v.coinObjectId,
      version: v.version,
    };
  });
}

/**
 * @returns The stakes array with the stakedSui amount reduced by amount. If amount is undefined, the stakedSui is removed.
 */
function subtractStake(stakes: StakeObject[], stakedSui: StakeObject, amount: number | undefined): StakeObject[] {
  return stakes.flatMap((s): StakeObject[] => {
    if (s.stakedSuiId === stakedSui.stakedSuiId) {
      if (amount === undefined) {
        // remove the stake
        return [];
      } else {
        // reduce the stake by amount
        return [{ ...s, principal: Number(s.principal) - amount }];
      }
    } else {
      // keep the stake unchanged
      return [s];
    }
  });
}

/**
 * Asserts that the stakesAfter array is the same as the stakesBefore array with the stakedSui amount reduced by amount.
 */
function assertReducedStake(
  stakesBefore: StakeObject[],
  stakesAfter: StakeObject[],
  stakedSui: StakeObject,
  amount: number | undefined
) {
  /*
   * Normalize the stake objects by converting the principal to a number and removing the estimatedReward.
   */
  function normStake(s: StakeObject): StakeObject {
    return { ...s, principal: Number(s.principal), estimatedReward: undefined };
  }
  stakesBefore = stakesBefore.map(normStake);
  stakesAfter = stakesAfter.map(normStake);
  assert.deepStrictEqual(stakesAfter, subtractStake(stakesBefore, stakedSui, amount));
}

async function getStakes(
  conn: RpcClient,
  owner: string,
  params: {
    filterStatus?: StakeObject['status'];
    filterMinValue?: number;
    attempts?: number;
    sleepMs?: number;
  } = {}
): Promise<StakeObject[]> {
  const all = await conn.getStakes(owner);
  const result = await Promise.all(
    all.flatMap((s) =>
      s.stakes
        .filter((v) => params.filterStatus === undefined || v.status === params.filterStatus)
        .filter((v) => params.filterMinValue === undefined || params.filterMinValue <= Number(v.principal))
    )
  );
  if (result.length) {
    return result;
  }
  const { attempts = 60, sleepMs = 1000 } = params;
  if (0 < attempts) {
    await new Promise((resolve) => setTimeout(resolve, sleepMs));
    return await getStakes(conn, owner, { ...params, attempts: attempts - 1, sleepMs });
  } else {
    throw new Error('getAllActiveStakedSuis: no active staked suis found');
  }
}

async function resolveStakedSui(conn: RpcClient, stake: StakeObject): Promise<SuiObjectRef> {
  return (await conn.getObject(stake.stakedSuiId)).data;
}

function getKeyPair(seed: string): KeyPair {
  const seedBuf = createHash('sha256').update(seed).digest();
  return new KeyPair({ seed: seedBuf });
}

async function signAndSubmit(
  conn: RpcClient,
  keyPair: KeyPair,
  txb: TransferBuilder | StakingBuilder | UnstakingBuilder
): Promise<void> {
  txb.sign({ key: keyPair.getKeys().prv });
  const tx = (await txb.build()) as TransferTransaction | StakingTransaction | UnstakingTransaction;
  debug('tx', util.inspect(tx.suiTransaction.tx, { depth: 10 }));
  const result = await conn.executeTransactionBlock(tx.toBroadcastFormat(), [
    Buffer.from(tx.serializedSig).toString('base64'),
  ]);
  if (result.effects?.status.status !== 'success') {
    throw new Error(`Transaction failed: ${JSON.stringify(result.effects?.status)}`);
  }
}

async function fundFromFaucet(url: string, v: KeyPair | string, amount = 100e9): Promise<void> {
  if (typeof v !== 'string') {
    v = v.getAddress();
  }
  await new Faucet(url).getCoins(v, 10e9);
}

describe('Sui Transaction Types', function () {
  if (process.env.DRONE) {
    console.log('skipping local_fullnode/transactions.ts on drone');
    return;
  }

  const keyPair = getKeyPair('test');
  const address = keyPair.getAddress();
  debug('address', address);

  const fullnodeUrl = process.env.SUI_FULLNODE_URL || 'http://127.0.0.1:9000';
  const faucetUrl = process.env.SUI_FAUCET_URL || 'http://127.0.0.1:9123';

  async function getDefaultGasData(keyPair: KeyPair): Promise<GasData> {
    return {
      owner: keyPair.getAddress(),
      payment: await getAllCoins(conn, keyPair.getAddress()),
      budget: 100_000_000,
      price: DUMMY_SUI_GAS_PRICE,
    };
  }

  let conn: RpcClient;
  let validator: string;
  before('establish connection', async function () {
    conn = await RpcClient.createCheckedConnection(fullnodeUrl);
    const { apys } = await conn.getValidatorsApy();
    validator = apys[0].address;
  });

  before('fund via faucet', async function () {
    if (faucetUrl) {
      await fundFromFaucet(faucetUrl, address);
    }
  });

  it('has coins', async function () {
    const { data } = await conn.getCoins(address);
    assert.notStrictEqual(data.length, 0);
  });

  it('can transfer coins', async function () {
    const builder = getBuilderFactory('tsui').getTransferBuilder();
    const txb = builder
      .type(SuiTransactionType.Transfer)
      .sender(address)
      .send([{ address, amount: (111_111).toString() }])
      .gasData(await getDefaultGasData(keyPair));

    await signAndSubmit(conn, keyPair, txb);
  });

  async function stakeAmount(keyPair: KeyPair, amount: number): Promise<void> {
    const builder = getBuilderFactory('tsui').getStakingBuilder();
    const txb = builder
      .type(SuiTransactionType.AddStake)
      .sender(keyPair.getAddress())
      .stake([{ amount, validatorAddress: validator }])
      .gasData(await getDefaultGasData(keyPair));

    await signAndSubmit(conn, keyPair, txb);
  }

  it('can stake coins', async function () {
    await stakeAmount(keyPair, 1e9);
  });

  function testUnstake(amount: number | undefined) {
    describe(`unstake (amount=${amount})`, function () {
      const keyPairStakeTest = getKeyPair(`stake-test-amount-${amount !== undefined}`);
      const address = keyPairStakeTest.getAddress();

      before('stake coins', async function () {
        await fundFromFaucet(faucetUrl, address);
        await stakeAmount(keyPairStakeTest, 10e9);
      });

      it(`can unstake`, async function () {
        const activeStakedSui = await getStakes(conn, address, {
          filterStatus: 'Active',
          filterMinValue: MIN_STAKING_THRESHOLD + (amount || 0),
        });
        assert(activeStakedSui.length > 0, 'No staked coins found');

        for (const stakedSui of activeStakedSui.slice(0, 3)) {
          debug('unstaking', stakedSui);
          const builder = getBuilderFactory('tsui').getUnstakingBuilder();
          const stakedBefore = await getStakes(conn, address);
          const txb = builder
            .type(SuiTransactionType.WithdrawStake)
            .sender(address)
            .unstake({ stakedSui: await resolveStakedSui(conn, stakedSui), amount })
            .gasData(await getDefaultGasData(keyPairStakeTest));
          await signAndSubmit(conn, keyPairStakeTest, txb);

          assertReducedStake(stakedBefore, await getStakes(conn, address), stakedSui, amount);
        }
      });
    });
  }

  testUnstake(undefined);
  testUnstake(1e9);
});

Выполнить команду


Для локальной разработки. Не используйте в интернете!