Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Additional Creditcoin js examples #1346

Draft
wants to merge 19 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 145 additions & 1 deletion creditcoin-js/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,150 @@
# creditcoin-js

**WARNING**: This is an alpha version of creditcoin-js. It is not ready for production use. It will have breaking changes often.
## Getting started

### Preqrequisites

Creditcoin-js requires the following to be installed:

- [Node.js](https://nodejs.org/en/)
- [TypeScript](https://www.typescriptlang.org/)

### Install

Adding Creditcoin-JS to your project is easy. Install it by using your favorite package manager:

```shell
yarn add creditcoin-js
```

This will install the latest release version, which should allow you to interact with Creditcoin's main network and your own local chains that use the latest Creditcoin binaries.

## Usage

### Import

Importing the library into your project:

```typescript
import { creditcoinApi } from 'creditcoin-js';

const { api } = await CreditcoinApi('ws://localhost:9944');

// don't forget to disconnect when you are done
await api.disconnect();
```

### Using the API

The API is a collection of modules that provide access to the various functions of the Creditcoin blockchain.

```typescript
const { api, extrinsics, utils } = await CreditcoinApi('ws://localhost:9944');
```

### Creating transactions

```typescript
const { api } = await CreditcoinApi('ws://localhost:9944');

const tx = api.
.tx
.balances
.transfer(
"5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
"1000000000000000" // CTC amount in microunits
// (1 CTC = 1e18 microunits)
)
```

### Signing & sending

```typescript
import { Keyring } from 'creditcoin-js';

const keyring = new Keyring({ type: 'sr25519' });
const alice = keyring.addFromUri('//Alice');

await tx.signAndSend(alice);
```

### Batching transactions

```typescript
const tx1 = api.tx.balances.transfer(addrBob, 10);
const tx2 = api.tx.balances.transfer(addrCharlie, 10);
const txs = [tx1, tx2];

const batch_tx = api.tx.utility.batch(txs);

await batch_tx.signAndSend(alice);
```

### Registering External Addresses
See `src/examples/register-address-v2.ts` for an example or the integration test at `../integration-tests/src/test/register-address-v2.test.s`.

### Swap GCRE -> CTC
See `src/examples/collect-coins-v2.ts` for an example or the integration test at `../integration-tests/src/test/gate-token.test.ts`.

### Reading Runtime Constants
```typescript
import { U64, U32 } from "@polkadot/types-codec";

const { api } = ccApi;

const expectedBlockTime = (api.consts.babe.expectedBlockTime as U64).toNumber()
const unverifiedTaskTimeout = (api.consts.creditcoin.unverifiedTaskTimeout as u32).toNumber();

const taskTimeout = expectedBlockTime * unverifiedTaskTimeout;
```


### Setting Offchain Local Storage (Ethereum RPC URI)
```typescript
function u8aToHex = (bytes: Uint8Array): string {
return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '0x');
};

const rpcUri = u8aToHex(api.createType('String', 'http://localhost:8545').toU8a());
await api.rpc.offchain.localStorageSet('PERSISTENT', 'ethereum-rpc-uri', rpcUri);
```

### Submitting Sudo Calls (Set CollectCoins Contract)
See `src/examples/set-collect-coins-contract.ts` for an example or the integration test at `../integration-tests/src/test/set-collect-coins-contract.test.ts`.

## Development

### Build

To build the project, run the following command from the root directory:

```shell
yarn build
```

### Updating Type definitions

Creditcoin-JS uses actual chain metadata to generate the API types and augmented endpoints. When the Creditcoin blockchain gets updated and includes new extrinsics or storage fields in it’s pallets, Creditcoin-JS must regenerate its types to include the newly available methods.

1. Fetch Chain Metadata

This process begins with pulling the current metadata from a running creditcoin-node by making an RPC call:

```shell
curl -H "Content-Type: application/json" -d '{"id":"1", "jsonrpc":"2.0", "method": "state_getMetadata", "params":[]}' http://localhost:9933
```

2. Add Metadata to creditcoin.json

The full JSON output must be saved into a creditcoin.json file as specified by the generation scripts included in package.json.

3. Generate Types

The types can be generated by running the following command:

```shell
yarn build:types
```

## Errors & Troubleshooting

Expand Down
21 changes: 21 additions & 0 deletions creditcoin-js/src/examples/collect-coins-v2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { CreditcoinApi } from '../types';
import { CollectCoinsContract } from '../model';
import { KeyringPair } from '@polkadot/keyring/types';

export async function collectCoinsV2Example(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for all of these examples, it would be best if they were written expecting no arguments. The examples original provided by Pablo all follow the same signature of an async function with no parameters, and I think we should maintain that where we can.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be possible for the register-address and set-collect-coins-contract examples because they are light on dependencies. The collect coins example needs the fully deployed token setup and also calls to setGateContract and setGateFaucet. I think all that extra code might take away from the point of the example.

ccApi: CreditcoinApi,
burnDetails: CollectCoinsContract,
creditcoinSigner: KeyringPair,
) {
const {
extrinsics: { requestCollectCoinsV2 },
} = ccApi;

// Submit the swap request, adding it to the task queue of the off chain worker
const collectCoins = await requestCollectCoinsV2(burnDetails, creditcoinSigner);

// Wait for the offchain worker to finish processing this request
// Under the hood waitForVerification tracks CollectedCoinsMinted and CollectedCoinsFailedVerification events using the TaskId as a unique key
// 900_000 (milliseconds) comes from an assumed 60 block task timeout deadline and assumed 15 second blocktime (check the constants provided by the runtime in production code)
return await collectCoins.waitForVerification(900_000);
}
26 changes: 26 additions & 0 deletions creditcoin-js/src/examples/register-address-v2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { CreditcoinApi } from '../types';
import { Wallet } from 'ethers';
import { Blockchain } from '../model';
import { personalSignAccountId } from '../utils';
import { KeyringPair } from '@polkadot/keyring/types';
import { personalSignSignature } from '../extrinsics/register-address-v2';

export async function registerAddressV2Example(
ccApi: CreditcoinApi,
ethSigner: Wallet,
creditcoinAddress: KeyringPair,
blockchain: Blockchain,
) {
const {
api,
extrinsics: { registerAddressV2 },
} = ccApi;

const accountId = creditcoinAddress.addressRaw;
const externalAddress = ethSigner.address;

const signature = await personalSignAccountId(api, ethSigner, accountId);
const proof = personalSignSignature(signature);

return registerAddressV2(externalAddress, blockchain, proof, creditcoinAddress);
}
19 changes: 19 additions & 0 deletions creditcoin-js/src/examples/set-collect-coins-contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { KeyringPair } from '@polkadot/keyring/types';
import { Blockchain } from 'src/model';
import { CreditcoinApi } from 'src/types';

export async function setCollectCoinsContractExample(
ccApi: CreditcoinApi,
contractAddress: string,
blockchain: Blockchain,
sudoSigner: KeyringPair,
) {
const { api } = ccApi;

const contract = api.createType('PalletCreditcoinOcwTasksCollectCoinsDeployedContract', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we get some comments here about what the createType call is doing and how the string argument was determined? It's a bit ambiguous for someone trying to learn to use the library.

address: contractAddress,
chain: blockchain,
});

await api.tx.sudo.sudo(api.tx.creditcoin.setCollectCoinsContract(contract)).signAndSend(sudoSigner, { nonce: -1 });
}
67 changes: 53 additions & 14 deletions integration-tests/src/test/gate-token.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { mnemonicGenerate } from '@polkadot/util-crypto';
import { signAccountId } from 'creditcoin-js/lib/utils';
import { GATEContract } from 'creditcoin-js/lib/extrinsics/request-collect-coins-v2';
import { testIf } from '../utils';
import { collectCoinsV2Example } from 'creditcoin-js/lib/examples/collect-coins-v2';

describe('Test GATE Token', (): void => {
let ccApi: CreditcoinApi;
Expand Down Expand Up @@ -37,7 +38,20 @@ describe('Test GATE Token', (): void => {
if ((global as any).CREDITCOIN_EXECUTE_SETUP_AUTHORITY) {
sudoSigner = (global as any).CREDITCOIN_CREATE_SIGNER(keyring, 'lender');
}
});

const { api } = ccApi;

await api.tx.sudo
.sudo(api.tx.balances.setBalance(gateFaucet.address, 1000, 0))
.signAndSend(sudoSigner, { nonce: -1 });

// Set the on chain location for the burn contract to be the address of the deployer wallet
const contract = api.createType('PalletCreditcoinOcwTasksCollectCoinsDeployedContract', {
address: gateToken.address,
chain: testingData.blockchain,
});
await api.tx.sudo.sudo(api.tx.creditcoin.setGateContract(contract)).signAndSend(sudoSigner, { nonce: -1 });
}, 900_000);

afterAll(async () => {
await ccApi.api.disconnect();
Expand All @@ -52,17 +66,6 @@ describe('Test GATE Token', (): void => {
extrinsics: { requestCollectCoinsV2 },
} = ccApi;

await api.tx.sudo
.sudo(api.tx.balances.setBalance(gateFaucet.address, 1000, 0))
.signAndSend(sudoSigner, { nonce: -1 });

// Set the on chain location for the burn contract to be the address of the deployer wallet
const contract = api.createType('PalletCreditcoinOcwTasksCollectCoinsDeployedContract', {
address: gateToken.address,
chain: testingData.blockchain,
});
await api.tx.sudo.sudo(api.tx.creditcoin.setGateContract(contract)).signAndSend(sudoSigner, { nonce: -1 });

const mintTx = await gateToken.mint(deployer.address, 2500);
await mintTx.wait(3);
const balance = await gateToken.balanceOf(deployer.address);
Expand Down Expand Up @@ -91,8 +94,9 @@ describe('Test GATE Token', (): void => {
.sudo(api.tx.creditcoin.setGateFaucet(gateFaucet.address))
.signAndSend(sudoSigner, { nonce: -1 });

const swapGATEEvent = await requestCollectCoinsV2(gateContract, sudoSigner);
const swapGATEVerified = await swapGATEEvent.waitForVerification(800_000).catch();

const swapGATE = await requestCollectCoinsV2(gateContract, sudoSigner);
const swapGATEVerified = await swapGATE.waitForVerification(900_000);

// Test #2: This is a successful transfer and should proceed normally
expect(swapGATEVerified).toBeTruthy();
Expand All @@ -107,4 +111,39 @@ describe('Test GATE Token', (): void => {
},
900_000,
);

// This test must run after the end to end test
// We are relying on the gate contract already being set, acceptable assumption since we run tests with --runInBand
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this requirement is no longer needed b/c the gate contract is now set in beforeAll.

In addition eventhough we use --runInBand I'm not quite sure how reliable that is, however that's a side topic.

FTR: I've started prefixing tests with a numeric prefix if the order matters, e.g. 001 - end-to-end inside collect-coins.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I misspoke in the comments. We need the gate contract to be set AND the faucet account to be set. The first sub-test of the e2e test is whether the extrinsic correctly reports that the faucet hasn't yet been set. After the error has been reported we set the faucet and try again this time expecting a success. So we need that sub-test to run before the example

testIf(
(global as any).CREDITCOIN_EXECUTE_SETUP_AUTHORITY,
'collectCoinsV2Example',
async () => {
const {
api,
} = ccApi;

const mintTx = await gateToken.mint(deployer.address, 2500);
await mintTx.wait(3);

const burnTx = await gateToken.burn(burnAmount);
await burnTx.wait(3);

// We are using the same deployer address as GCRE so the address may already be registered
await tryRegisterAddress(
ccApi,
deployer.address,
testingData.blockchain,
signAccountId(api, deployer, sudoSigner.address),
sudoSigner,
(global as any).CREDITCOIN_REUSE_EXISTING_ADDRESSES,
);

const gateContract = GATEContract(deployer.address, burnTx.hash);
const swapGATEVerified = await collectCoinsV2Example(ccApi, gateContract, sudoSigner);

expect(swapGATEVerified).toBeTruthy();
expect(swapGATEVerified.amount.toNumber()).toEqual(burnAmount / 2);
},
900_000,
);
});
35 changes: 25 additions & 10 deletions integration-tests/src/test/register-address-v2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { Blockchain, KeyringPair, Wallet, creditcoinApi } from 'creditcoin-js';
import {
createAddressId,
ethSignSignature,
personalSignSignature,
createCreditCoinOwnershipProof,
personalSignSignature,
} from 'creditcoin-js/lib/extrinsics/register-address-v2';
import { checkAddress, testData } from 'creditcoin-js/lib/testUtils';
import { CreditcoinApi } from 'creditcoin-js/lib/types';
import { signAccountId, personalSignAccountId } from 'creditcoin-js/lib/utils';
import { personalSignAccountId, signAccountId } from 'creditcoin-js/lib/utils';
import { extractFee } from '../utils';
import { registerAddressV2Example } from 'creditcoin-js/lib/examples/register-address-v2';

describe('RegisterAddressV2', () => {
let ccApi: CreditcoinApi;
Expand Down Expand Up @@ -68,16 +69,17 @@ describe('RegisterAddressV2', () => {
});

it('registerAddressV2 PersonalSign works as expected', async (): Promise<void> => {
const {
api,
extrinsics: { registerAddressV2 },
} = ccApi;

const lenderWallet = Wallet.createRandom();
const accountId = await personalSignAccountId(api, lenderWallet, lender.addressRaw);
const proof = personalSignSignature(accountId);

const lenderRegAddr = await registerAddressV2(lenderWallet.address, blockchain, proof, lender);
const { api, extrinsics: { registerAddressV2 } } = ccApi;

const accountId = lender.addressRaw;
const externalAddress = lenderWallet.address;

const signature = await personalSignAccountId(api, lenderWallet, accountId);
const proof = personalSignSignature(signature);

const lenderRegAddr = await registerAddressV2(externalAddress, blockchain, proof, lender);

// manually constructed address is the same as returned by Creditcoin
const addressId = createAddressId(blockchain, lenderWallet.address);
Expand All @@ -87,4 +89,17 @@ describe('RegisterAddressV2', () => {
const result = await checkAddress(ccApi, addressId);
expect(result).toBeDefined();
});

it('registerAddressV2Example works as expected', async () => {
const ethSigner = Wallet.createRandom();
const lenderRegAddr = await registerAddressV2Example(ccApi, ethSigner, lender, blockchain);

// manually constructed address is the same as returned by Creditcoin
const addressId = createAddressId(blockchain, ethSigner.address);
expect(addressId).toBe(lenderRegAddr.itemId);

// manually constructed address should be reported as registered
const result = await checkAddress(ccApi, addressId);
expect(result).toBeDefined();
});
});
Loading
Loading