forked from aptos-labs/aptos-core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
21 changed files
with
856 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
node_modules | ||
packages/**/node_modules/ | ||
dist/** | ||
**/*.test.ts |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"], | ||
}, | ||
}, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
coverage |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
v16.14.0n |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(), | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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\""); | ||
|
||
}); | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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!`); | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
|
||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
] | ||
} |