Skip to content

Commit

Permalink
Showing 21 changed files with 856 additions and 0 deletions.
10 changes: 10 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -183,6 +183,15 @@ jobs:
-X POST -d "{\"body\": \"${FORGE_COMMENT}\"}" \
"https://api.github.com/repos/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/issues/${PR_NUMBER}/comments"
exit 0
sdk-typescript-test:
docker:
- image: circleci/node:16.13.1-browsers
steps:
- checkout
- run: cd ./ecosystem/typescript/sdk && yarn install
- run: cd ./ecosystem/typescript/sdk && yarn lint
- run: cd ./ecosystem/typescript/sdk && yarn test
- run: cd ./ecosystem/typescript/sdk && yarn build

workflows:
### on bors action ###
@@ -200,6 +209,7 @@ workflows:
- e2e-test
- lint
- unit-test
- sdk-typescript-test
- docker-build-push:
context: aws-dev
- forge-k8s:
4 changes: 4 additions & 0 deletions ecosystem/typescript/sdk/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
packages/**/node_modules/
dist/**
**/*.test.ts
34 changes: 34 additions & 0 deletions ecosystem/typescript/sdk/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
module.exports = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
"airbnb-base",
],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: "latest",
sourceType: "module",
},
plugins: [
"@typescript-eslint",
],
rules: {
quotes: ["error", "double"],
"max-len": ["error", 120],
"import/extensions": [
"error",
"never",
],
"max-classes-per-file": ["error", 10],
},
settings: {
"import/resolver": {
node: {
extensions: [".js", ".jsx", ".ts", ".tsx"],
},
},
},
};
14 changes: 14 additions & 0 deletions ecosystem/typescript/sdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.DS_Store
*/**/.DS_Store
npm-debug.log
.npm/
/coverage
/tmp
node_modules
.idea/
.history/
.vscode/
dist/
.nyc_output/
build/
yarn.lock
1 change: 1 addition & 0 deletions ecosystem/typescript/sdk/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
coverage
1 change: 1 addition & 0 deletions ecosystem/typescript/sdk/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v16.14.0n
85 changes: 85 additions & 0 deletions ecosystem/typescript/sdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Aptos TS/JS SDK

[![Discord][discord-image]][discord-url]
[![NPM Package Version][npm-image-version]][npm-url]
[![NPM Package Downloads][npm-image-downloads]][npm-url]

You need to connect to an [Aptos](https:/github.com/aptos-labs/aptos-core/) node to use this library, or run one
yourself locally.

Please read the [documentation][docs] for more.

## Usage
```js
// In Node.js
const Aptos = require("aptos");

const aptos = new AptosClient("https://fullnode.devnet.aptoslabs.com/");
console.log(aptos);
> { ... }
```

There you go, now you can use it:

```js
const account = new AptosAccount();
const accountInfo = await client.getAccount(account.address());
```

### Usage with TypeScript
You can use `aptos` like any other TypeScript module:

```typescript
import { AptosAccount, AptosClient } from "aptos"
const aptos = new AptosClient("https://fullnode.devnet.aptoslabs.com/");
const account = new AptosAccount();
const accountInfo = await client.getAccount(account.address());
```

If you are using the types in a `commonjs` module, like in a Node app, you just have to enable `esModuleInterop`
and `allowSyntheticDefaultImports` in your `tsconfig` for types compatibility:

```js
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
...
```
### Requirements
- [Node.js](https://nodejs.org)
- [yarn](https://yarnpkg.com/)
```bash
sudo apt-get update
sudo apt-get install nodejs yarn
```
### Generating Types
Originally created with this:
```bash
$ npx swagger-typescript-api -p ../api/doc/openapi.yaml -o ./src/api --modular --axios --single-http-client
```
#### Changes to make after generation:
- OpenAPI/SpecHTML routes/types deleted as they're unneeded.
- There are a few type errors in the `http-client.ts` as the axios types are incomplete, that were fixed via `// @ts-ignore`


### Testing (jest)

```bash
yarn test
```


[repo]: https://github.com/aptos-labs/aptos-core
[npm-image-version]: https://img.shields.io/npm/v/aptos.svg
[npm-image-downloads]: https://img.shields.io/npm/dm/aptos.svg
[npm-url]: https://npmjs.org/package/aptos
[discord-image]: https://img.shields.io/discord/945856774056083548?label=Discord&logo=discord&style=flat~~~~
[discord-url]: https://discord.gg/aptoslabs

## Semantic versioning

This project follows [semver](https://semver.org/) as closely as possible
16 changes: 16 additions & 0 deletions ecosystem/typescript/sdk/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/** @type {import("ts-jest/dist/types").InitialOptionsTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
coveragePathIgnorePatterns: ["api/*"],
testPathIgnorePatterns: ["dist/*"],
collectCoverage: true,
coverageThreshold: {
global: {
branches: 90,
functions: 95,
lines: 95,
statements: 95,
},
},
};
51 changes: 51 additions & 0 deletions ecosystem/typescript/sdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "aptos",
"description": "Aptos SDK",
"license": "Apache-2.0",
"engines": {
"node": ">=11.0.0"
},
"main": "./dist/aptos.min.js",
"types": "./dist/aptos.d.ts",
"scripts": {
"build": "tsc -p .",
"lint": "eslint \"**/*.ts\"",
"test": "jest",
"cov:clean": "rm -rf coverage"
},
"repository": {
"type": "git",
"url": "https://github.com/aptos-labs/aptos-core.git"
},
"homepage": "https://github.com/aptos-labs/aptos-core",
"bugs": {
"url": "https://github.com/aptos-labs/aptos-core/issues"
},
"author": "aptoslabs.com",
"keywords": [
"Aptos",
"Aptos Labs",
"Move"
],
"devDependencies": {
"@types/jest": "^27.4.1",
"@types/node": "^17.0.23",
"@typescript-eslint/eslint-plugin": "^5.17.0",
"@typescript-eslint/parser": "^5.17.0",
"eslint": "^7.32.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.25.4",
"jest": "^27.5.1",
"ts-jest": "^27.1.4",
"ts-loader": "^9.2.8",
"ts-node": "^10.7.0",
"typescript": "^4.6.3"
},
"dependencies": {
"axios": "^0.26.1",
"buffer": "^6.0.3",
"js-sha3": "^0.8.0",
"tweetnacl": "^1.0.3"
},
"version": "0.0.9"
}
48 changes: 48 additions & 0 deletions ecosystem/typescript/sdk/src/aptos_account.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import AptosAccount, { AptosAccountPrivateKeyObject } from "./aptos_account";

const aptosAccountPrivateKeyObject: AptosAccountPrivateKeyObject = {
"address": "0x978c213990c4833df71548df7ce49d54c759d6b6d932de22b24d56060b7af2aa",
"privateKeyHex": "0xc5338cd251c22daa8c9c9cc94f498cc8a5c7e1d2e75287a5dda91096fe64efa5de19e5d1880cac87d57484ce9ed2e84cf0f9599f12e7cc3a52e4e7657a763f2c",
"publicKeyHex": "0xde19e5d1880cac87d57484ce9ed2e84cf0f9599f12e7cc3a52e4e7657a763f2c",
};


test("generates random accounts", () => {
const a1 = new AptosAccount();
const a2 = new AptosAccount();
expect(a1.authKey()).not.toBe(a2.authKey());
expect(a1.address().hex()).not.toBe(a2.address().hex());
});

test("accepts custom address", () => {
const address = "0x777";
const a1 = new AptosAccount(null, address);
expect(a1.address().hex()).toBe(address);
});

test("Deserializes from AptosAccountPrivateKeyObject", () => {
const a1 = AptosAccount.fromAptosAccountPrivateKeyObject(aptosAccountPrivateKeyObject);
expect(a1.address().hex()).toBe(aptosAccountPrivateKeyObject.address);
expect(a1.pubKey().hex()).toBe(aptosAccountPrivateKeyObject.publicKeyHex);
});

test("Deserializes from AptosAccountPrivateKeyObject without address", () => {
const privateKeyObject = { privateKeyHex: aptosAccountPrivateKeyObject.privateKeyHex };
const a1 = AptosAccount.fromAptosAccountPrivateKeyObject(privateKeyObject);
expect(a1.address().hex()).toBe(aptosAccountPrivateKeyObject.address);
expect(a1.pubKey().hex()).toBe(aptosAccountPrivateKeyObject.publicKeyHex);
});

test("Serializes/Deserializes", () => {
const a1 = new AptosAccount();
const a2 = AptosAccount.fromAptosAccountPrivateKeyObject(a1.toPrivateKeyObject());
expect(a1.authKey().hex()).toBe(a2.authKey().hex());
expect(a1.address().hex()).toBe(a2.address().hex());
});

test("Signs Strings", () => {
const a1 = AptosAccount.fromAptosAccountPrivateKeyObject(aptosAccountPrivateKeyObject);
expect(a1.signHexString("0x77777").hex())
.toBe(
"0xc5de9e40ac00b371cd83b1c197fa5b665b7449b33cd3cdd305bb78222e06a671a49625ab9aea8a039d4bb70e275768084d62b094bc1b31964f2357b7c1af7e0d");
});
72 changes: 72 additions & 0 deletions ecosystem/typescript/sdk/src/aptos_account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import * as Nacl from "tweetnacl";
import * as SHA3 from "js-sha3";
import { Buffer } from "buffer/"; // the trailing slash is important!
import { HexString, MaybeHexString } from "./hex_string";
import Types from "./types";

export interface AptosAccountPrivateKeyObject {
address?: string,
publicKeyHex?: Types.HexEncodedBytes,
privateKeyHex: Types.HexEncodedBytes,
}

export default class AptosAccount {
readonly signingKey: Nacl.SignKeyPair;

private readonly accountAddress: HexString;

private authKeyCached?: HexString;

static fromAptosAccountPrivateKeyObject(obj: AptosAccountPrivateKeyObject): AptosAccount {
return new AptosAccount(HexString.ensure(obj.privateKeyHex).toUint8Array(), obj.address);
}

/** This class allows passing in an address, to handle account key rotation, where auth_key != public_key */
constructor(privateKeyBytes?: Uint8Array | undefined, address?: MaybeHexString) {
if (privateKeyBytes) {
this.signingKey = Nacl.sign.keyPair.fromSeed(privateKeyBytes.slice(0, 32));
} else {
this.signingKey = Nacl.sign.keyPair();
}
this.accountAddress = HexString.ensure(address || this.authKey().hex());
}

/** Returns the address associated with the given account */
address(): HexString {
return this.accountAddress;
}

/** Returns the authKey for the associated account */
authKey(): HexString {
if (!this.authKeyCached) {
const hash = SHA3.sha3_256.create();
hash.update(Buffer.from(this.signingKey.publicKey));
hash.update("\x00");
this.authKeyCached = new HexString(hash.hex());
}
return this.authKeyCached;
}

/** Returns the public key for the associated account */
pubKey(): HexString {
return HexString.ensure(Buffer.from(this.signingKey.publicKey).toString("hex"));
}

signBuffer(buffer: Buffer): HexString {
const signature = Nacl.sign(buffer, this.signingKey.secretKey);
return HexString.ensure(Buffer.from(signature).toString("hex").slice(0, 128));
}

signHexString(hexString: MaybeHexString): HexString {
const toSign = HexString.ensure(hexString).toBuffer();
return this.signBuffer(toSign);
}

toPrivateKeyObject(): AptosAccountPrivateKeyObject {
return {
address: this.address().hex(),
publicKeyHex: this.pubKey().hex(),
privateKeyHex: HexString.fromUint8Array(this.signingKey.secretKey.slice(0, 32)).hex(),
};
}
}
67 changes: 67 additions & 0 deletions ecosystem/typescript/sdk/src/aptos_client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { AptosClient, raiseForStatus } from "./aptos_client";
import { AnyObject } from "./util";
import { AxiosResponse } from "axios";

const nodeUrl = "https://fullnode.devnet.aptoslabs.com";

test("gets genesis account", async () => {
const client = new AptosClient(nodeUrl);
const account = await client.getAccount("0x1");
expect(account.authentication_key.length).toBe(66);
expect(account.sequence_number).not.toBeNull();
});

test("gets transactions", async () => {
const client = new AptosClient(nodeUrl);
const transactions = await client.getTransactions();
expect(transactions.length).toBeGreaterThan(0);
});

test("gets genesis resources", async () => {
const client = new AptosClient(nodeUrl);
const resources = await client.getAccountResources("0x1");
const accountResource = resources.find((r) => r.type === "0x1::Account::Account");
expect((accountResource.data as AnyObject)["self_address"]).toBe("0x1");
});

test("gets account modules", async () => {
const client = new AptosClient(nodeUrl);
const modules = await client.getAccountModules("0x1");
const module = modules.find((r) => r.abi.name === "TestCoin");
expect(module.abi.address).toBe("0x1");
});

test("test raiseForStatus", async () => {
const testData = { "hello": "wow" };
const fakeResponse: AxiosResponse = {
status: 200,
statusText: "Status Text",
data: "some string",
request: {
host: "host",
path: "/path"
},
} as AxiosResponse;

// Shouldn't throw
raiseForStatus(200, fakeResponse, testData);
raiseForStatus(200, fakeResponse);

// an error, oh no!
fakeResponse.status = 500;
expect(() => raiseForStatus(200, fakeResponse, testData))
.toThrow("Status Text - \"some string\" @ host/path : {\"hello\":\"wow\"}");

expect(() => raiseForStatus(200, fakeResponse))
.toThrow("Status Text - \"some string\" @ host/path");

// Just a wild test to make sure it doesn't break: request is `any`!
delete fakeResponse.request;
expect(() => raiseForStatus(200, fakeResponse, testData))
.toThrow("Status Text - \"some string\" : {\"hello\":\"wow\"}");

expect(() => raiseForStatus(200, fakeResponse))
.toThrow("Status Text - \"some string\"");

});

223 changes: 223 additions & 0 deletions ecosystem/typescript/sdk/src/aptos_client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import { AxiosRequestConfig, AxiosResponse } from "axios";
import { Accounts } from "./api/Accounts";
import { Events } from "./api/Events";
import { Transactions } from "./api/Transactions";
import { HttpClient } from "./api/http-client";
import { HexString, MaybeHexString } from "./hex_string";
import { sleep } from "./util";
import AptosAccount from "./aptos_account";
import Types from "./types";

export class RequestError extends Error {
response?: AxiosResponse<any, Types.AptosError>;

requestBody?: string;

constructor(message?: string, response?: AxiosResponse<any, Types.AptosError>, requestBody?: string) {
const data = JSON.stringify(response.data);
const hostAndPath = [response.request?.host, response.request?.path].filter((e) => !!e).join("");
super(`${message} - ${data}${hostAndPath ? ` @ ${hostAndPath}` : ""}${requestBody ? ` : ${requestBody}` : ""}`);
this.response = response;
this.requestBody = requestBody;
Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain
}
}

export type AptosClientConfig = Omit<AxiosRequestConfig, "data" | "cancelToken" | "method">;

export function raiseForStatus<T>(
expectedStatus: number,
response: AxiosResponse<T, Types.AptosError>,
requestContent?: any,
) {
if (response.status !== expectedStatus) {
if (requestContent) {
throw new RequestError(response.statusText, response, JSON.stringify(requestContent));
}
throw new RequestError(response.statusText, response);
}
}

export class AptosClient {
nodeUrl: string;

client: HttpClient;

// These are the different routes
accounts: Accounts;

events: Events;

transactions: Transactions;

constructor(nodeUrl: string, config?: AptosClientConfig) {
this.nodeUrl = nodeUrl;

// `withCredentials` ensures cookie handling
this.client = new HttpClient<unknown>({
withCredentials: true,
baseURL: nodeUrl,
validateStatus: () => true, // Don't explode here on error responses; let our code handle it
...(config || {}),
});

// Initialize routes
this.accounts = new Accounts(this.client);
this.events = new Events(this.client);
this.transactions = new Transactions(this.client);
}

/** Returns the sequence number and authentication key for an account */
async getAccount(accountAddress: MaybeHexString): Promise<Types.Account> {
const response = await this.accounts.getAccount(HexString.ensure(accountAddress).hex());
raiseForStatus(200, response);
return response.data;
}

/** Returns transactions sent by the account */
async getAccountTransactions(
accountAddress: MaybeHexString,
query?: { start?: number; limit?: number },
): Promise<Types.OnChainTransaction[]> {
const response = await this.accounts.getAccountTransactions(HexString.ensure(accountAddress).hex(), query);
raiseForStatus(200, response);
return response.data;
}

/** Returns all modules associated with the account */
async getAccountModules(
accountAddress: MaybeHexString,
query?: { version?: Types.LedgerVersion },
): Promise<Types.MoveModule[]> {
const response = await this.accounts.getAccountModules(HexString.ensure(accountAddress).hex(), query);
raiseForStatus(200, response);
return response.data;
}

/** Returns all resources associated with the account */
async getAccountResources(
accountAddress: MaybeHexString,
query?: { version?: Types.LedgerVersion },
): Promise<Types.AccountResource[]> {
const response = await this.accounts.getAccountResources(HexString.ensure(accountAddress).hex(), query);
raiseForStatus(200, response);
return response.data;
}

/** Generates a transaction request that can be submitted to produce a raw transaction that
* can be signed, which upon being signed can be submitted to the blockchain. */
async generateTransaction(
sender: MaybeHexString,
payload: Types.TransactionPayload,
options?: Partial<Types.UserTransactionRequest>,
): Promise<Types.UserTransactionRequest> {
const senderAddress = HexString.ensure(sender);
const account = await this.getAccount(senderAddress);
return {
sender: senderAddress.hex(),
sequence_number: account.sequence_number,
max_gas_amount: "1000",
gas_unit_price: "1",
gas_currency_code: "XUS",
// Unix timestamp, in seconds + 10 minutes
expiration_timestamp_secs: (Math.floor(Date.now() / 1000) + 600).toString(),
payload,
...(options || {}),
};
}

/** Converts a transaction request by `generate_transaction` into it's binary hex BCS representation, ready for
* signing and submitting.
* Generally you may want to use `signTransaction`, as it takes care of this step + signing */
async createSigningMessage(txnRequest: Types.UserTransactionRequest): Promise<Types.HexEncodedBytes> {
const response = await this.transactions.createSigningMessage(txnRequest);
raiseForStatus(200, response, txnRequest);

const { message } = response.data;
return message;
}

/** Converts a transaction request produced by `generate_transaction` into a properly signed
* transaction, which can then be submitted to the blockchain. */
async signTransaction(
accountFrom: AptosAccount,
txnRequest: Types.UserTransactionRequest,
): Promise<Types.SubmitTransactionRequest> {
const message = await this.createSigningMessage(txnRequest);
const signatureHex = accountFrom.signHexString(message.substring(2));

const transactionSignature: Types.TransactionSignature = {
type: "ed25519_signature",
public_key: accountFrom.pubKey().hex(),
signature: signatureHex.hex(),
};

return { signature: transactionSignature, ...txnRequest };
}

async getEventsByEventKey(eventKey: Types.HexEncodedBytes): Promise<Types.Event[]> {
const response = await this.events.getEventsByEventKey(eventKey);
raiseForStatus(200, response, `eventKey: ${eventKey}`);
return response.data;
}

async getEventsByEventHandle(
address: MaybeHexString,
eventHandleStruct: Types.MoveStructTagId,
fieldName: string,
): Promise<Types.Event[]> {
const response = await this.accounts.getEventsByEventHandle(
HexString.ensure(address).hex(),
eventHandleStruct,
fieldName,
);
raiseForStatus(200, response, { address, eventHandleStruct, fieldName });
return response.data;
}

/** Submits a signed transaction to the blockchain. */
async submitTransaction(
accountFrom: AptosAccount,
signedTxnRequest: Types.SubmitTransactionRequest,
): Promise<Types.PendingTransaction> {
const response = await this.transactions.submitTransaction(signedTxnRequest);
raiseForStatus(202, response, signedTxnRequest);
return response.data;
}

async getTransactions(query?: { start?: number; limit?: number }): Promise<Types.OnChainTransaction[]> {
const response = await this.transactions.getTransactions(query);
raiseForStatus(200, response);
return response.data;
}

async getTransaction(txnHashOrVersion: string): Promise<Types.Transaction> {
const response = await this.transactions.getTransaction(txnHashOrVersion);
raiseForStatus(200, response, { txnHashOrVersion });
return response.data;
}

async transactionPending(txnHash: Types.HexEncodedBytes): Promise<boolean> {
const response = await this.transactions.getTransaction(txnHash);

if (response.status === 404) {
return true;
}
raiseForStatus(200, response, txnHash);
return response.data.type === "pending_transaction";
}

/** Waits up to 10 seconds for a transaction to move past pending state */
async waitForTransaction(txnHash: Types.HexEncodedBytes) {
let count = 0;
// eslint-disable-next-line no-await-in-loop
while (await this.transactionPending(txnHash)) {
// eslint-disable-next-line no-await-in-loop
await sleep(1000);
count += 1;
if (count >= 10) {
throw new Error(`Waiting for transaction ${txnHash} timed out!`);
}
}
}
}
59 changes: 59 additions & 0 deletions ecosystem/typescript/sdk/src/faucet_client.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { AptosClient } from "./aptos_client";
import FaucetClient from "./faucet_client";
import AptosAccount from "./aptos_account";
import Types from "./types";
import { UserTransaction } from "./api/data-contracts";

const nodeUrl = "https://fullnode.devnet.aptoslabs.com";
const faucetUrl = "https://faucet.devnet.aptoslabs.com";


test("full tutorial faucet flow", async () => {
const client = new AptosClient(nodeUrl);
const faucetClient = new FaucetClient(nodeUrl, faucetUrl);

const account1 = new AptosAccount();
const txns = await faucetClient.fundAccount(account1.address(), 5000);
const tx1 = await client.getTransaction(txns[1]);
expect(tx1.type).toBe("user_transaction");
let resources = await client.getAccountResources(account1.address());
let accountResource = resources.find((r) => r.type === "0x1::TestCoin::Balance");
expect((accountResource.data as { "coin": { "value": string } }).coin.value).toBe("5000");

const account2 = new AptosAccount();
await faucetClient.fundAccount(account2.address(), 0);
resources = await client.getAccountResources(account2.address());
accountResource = resources.find((r) => r.type === "0x1::TestCoin::Balance");
expect((accountResource.data as { "coin": { "value": string } }).coin.value).toBe("0");

const payload: Types.TransactionPayload = {
type: "script_function_payload",
function: "0x1::TestCoin::transfer",
type_arguments: [],
arguments: [
account2.address().hex(),
"717",
]
};
const txnRequest = await client.generateTransaction(account1.address(), payload);
const signedTxn = await client.signTransaction(account1, txnRequest);
const transactionRes = await client.submitTransaction(account1, signedTxn);
await client.waitForTransaction(transactionRes.hash);

resources = await client.getAccountResources(account2.address());
accountResource = resources.find((r) => r.type === "0x1::TestCoin::Balance");
expect((accountResource.data as { "coin": { "value": string } }).coin.value).toBe("717");

const res = await client.getAccountTransactions(account1.address(), { start: 0 });
const tx = res.find((e) => e.type === "user_transaction") as UserTransaction;
expect(tx.sender).toBe(account1.address().hex());

const events = await client.getEventsByEventHandle(
tx.sender,
"0x1::TestCoin::TransferEvents",
"sent_events");
expect(events[0].type).toBe("0x1::TestCoin::SentEvent");

const events2 = await client.getEventsByEventKey(events[0].key);
expect(events2[0].type).toBe("0x1::TestCoin::SentEvent");
}, 30 * 1000);
33 changes: 33 additions & 0 deletions ecosystem/typescript/sdk/src/faucet_client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/** Faucet creates and funds accounts. This is a thin wrapper around that. */
import axios from "axios";
import {
AptosClient, AptosClientConfig, raiseForStatus,
} from "./aptos_client";
import Types from "./types";
import { HexString, MaybeHexString } from "./hex_string";

export default class FaucetClient extends AptosClient {
faucetUrl: string;

constructor(nodeUrl: string, faucetUrl: string, config?: AptosClientConfig) {
super(nodeUrl, config);
this.faucetUrl = faucetUrl;
}

/** This creates an account if it does not exist and mints the specified amount of
coins into that account */
async fundAccount(address: MaybeHexString, amount: number): Promise<Types.HexEncodedBytes[]> {
const url = `${this.faucetUrl}/mint?amount=${amount}&address=${HexString.ensure(address).noPrefix()}`;
const response = await axios.post<Array<string>>(url, {}, { validateStatus: () => true });
raiseForStatus(200, response);

const tnxHashes = response.data;
const promises = [];
for (let i = 0; i < tnxHashes.length; i += 1) {
const tnxHash = tnxHashes[i];
promises.push(this.waitForTransaction(tnxHash));
}
await Promise.all(promises);
return tnxHashes;
}
}
48 changes: 48 additions & 0 deletions ecosystem/typescript/sdk/src/hex_string.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { HexString } from "./hex_string";

const withoutPrefix = "007711b4d0";
const withPrefix = `0x${withoutPrefix}`;

function validate(hexString: HexString) {
expect(hexString.hex()).toBe(withPrefix);
expect(hexString.toString()).toBe(withPrefix);
expect(`${hexString}`).toBe(withPrefix);
expect(hexString.noPrefix()).toBe(withoutPrefix);
}


test("from/to buffer", () => {
const hs = new HexString(withPrefix);
expect(hs.toBuffer().toString("hex")).toBe(withoutPrefix);
expect(HexString.fromBuffer(hs.toBuffer()).hex()).toBe(withPrefix);
});

test("from/to Uint8Array", () => {
const hs = new HexString(withoutPrefix);
expect(HexString.fromUint8Array(hs.toUint8Array()).hex()).toBe(withPrefix);
});

test("accepts input without prefix", () => {
const hs = new HexString(withoutPrefix);
validate(hs);
});

test("accepts input with prefix", () => {
const hs = new HexString(withPrefix);
validate(hs);
});

test("ensures input when string", () => {
const hs = HexString.ensure(withoutPrefix);
validate(hs);
});

test("ensures input when HexString", () => {
const hs1 = new HexString(withPrefix);
const hs = HexString.ensure(hs1);
validate(hs);
});




53 changes: 53 additions & 0 deletions ecosystem/typescript/sdk/src/hex_string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { Buffer } from "buffer/"; // the trailing slash is important!
import Types from "./types";

// eslint-disable-next-line no-use-before-define
export type MaybeHexString = HexString | string | Types.HexEncodedBytes;

export class HexString {
/// We want to make sure this hexString has the `0x` hex prefix
private readonly hexString: string;

static fromBuffer(buffer: Buffer): HexString {
return new HexString(buffer.toString("hex"));
}

static fromUint8Array(arr: Uint8Array): HexString {
return HexString.fromBuffer(Buffer.from(arr));
}

static ensure(hexString: MaybeHexString): HexString {
if (typeof hexString === "string") {
return new HexString(hexString);
}
return hexString;
}

constructor(hexString: string | Types.HexEncodedBytes) {
if (hexString.startsWith("0x")) {
this.hexString = hexString;
} else {
this.hexString = `0x${hexString}`;
}
}

hex(): string {
return this.hexString;
}

noPrefix(): string {
return this.hexString.slice(2);
}

toString(): string {
return this.hex();
}

toBuffer(): Buffer {
return Buffer.from(this.noPrefix(), "hex");
}

toUint8Array(): Uint8Array {
return Uint8Array.from(this.toBuffer());
}
}
6 changes: 6 additions & 0 deletions ecosystem/typescript/sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// All parts of our package are accessible as imports, but we re-export our higher level API here for convenience
export * from "./aptos_account";
export * from "./hex_string";
export * from "./aptos_client";
export * from "./faucet_client";
export * from "./types";
4 changes: 4 additions & 0 deletions ecosystem/typescript/sdk/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// A convenience re-export from the lower level generated code
import * as Types from "./api/data-contracts";

export default Types;
9 changes: 9 additions & 0 deletions ecosystem/typescript/sdk/src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export type Nullable<T> = { [P in keyof T]: T[P] | null };

export type AnyObject = { [key: string]: any };

export async function sleep(timeMs: number): Promise<null> {
return new Promise((resolve) => {
setTimeout(resolve, timeMs);
});
}
18 changes: 18 additions & 0 deletions ecosystem/typescript/sdk/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"outDir": "./dist",
"allowJs": true,
"target": "es6",
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"sourceMap": true,
"module": "commonjs",
"pretty": true,
"noImplicitAny": true,
"allowSyntheticDefaultImports": true
},
"include": [
"src"
]
}

0 comments on commit abd8f0a

Please sign in to comment.