diff --git a/package-lock.json b/package-lock.json index aef18207..5dcf40e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -451,9 +451,9 @@ } }, "@improbable-eng/grpc-web": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.12.0.tgz", - "integrity": "sha512-uJjgMPngreRTYPBuo6gswMj1gK39Wbqre/RgE0XnSDXJRg6ST7ZhuS53dFE6Vc2CX4jxgl+cO+0B3op8LA4Q0Q==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@improbable-eng/grpc-web/-/grpc-web-0.13.0.tgz", + "integrity": "sha512-vaxxT+Qwb7GPqDQrBV4vAAfH0HywgOLw6xGIKXd9Q8hcV63CQhmS3p4+pZ9/wVvt4Ph3ZDK9fdC983b9aGMUFg==", "requires": { "browser-headers": "^0.4.0" } @@ -464,11 +464,11 @@ "integrity": "sha512-+Kjz+Dktfz5LKTZA9ZW/Vlww6HF9KaKz4x2mVe1O8CJdOP2WfzC+KY8L6EWMqVLrV4MvdBuQdSgDmvSJz+OGuA==" }, "@textile/grpc-powergate-client": { - "version": "0.0.1-beta.13", - "resolved": "https://registry.npmjs.org/@textile/grpc-powergate-client/-/grpc-powergate-client-0.0.1-beta.13.tgz", - "integrity": "sha512-ACYEOokCWkev1sxbZcTGFCPaUDpk95cOHnmKDXdCA2hS+qYdI0SJYVZXCmVkLQf2WQB8P6Losi+tc7Iw3A42dg==", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@textile/grpc-powergate-client/-/grpc-powergate-client-0.1.1.tgz", + "integrity": "sha512-PJ1VIKALEvYxGHi70y1tIBOFrpK7e4XvAGOfDnBpiVXqzjlTiNijDxzgJIWjNXLugkp4e1QZVrbJ7LG0+EQfBg==", "requires": { - "@improbable-eng/grpc-web": "^0.12.0", + "@improbable-eng/grpc-web": "^0.13.0", "@types/google-protobuf": "^3.7.2", "google-protobuf": "^3.12.2" } diff --git a/package.json b/package.json index 23ef512d..8d9d086b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "types": "dist/index", "scripts": { "prepublishOnly": "npm run build", - "prepare": "npm run compile", "prebuild": "npm run clean", "build": "npm run compile", "compile": "tsc -b tsconfig.json", @@ -66,6 +65,6 @@ }, "dependencies": { "@improbable-eng/grpc-web-node-http-transport": "^0.12.0", - "@textile/grpc-powergate-client": "0.0.1-beta.13" + "@textile/grpc-powergate-client": "0.1.1" } } \ No newline at end of file diff --git a/src/asks/index.spec.ts b/src/asks/index.spec.ts new file mode 100644 index 00000000..de45bdff --- /dev/null +++ b/src/asks/index.spec.ts @@ -0,0 +1,19 @@ +import { expect } from "chai" +import { createAsks } from "." +import { asksTypes } from "../types" +import { getTransport, host } from "../util" + +describe("asks", () => { + const c = createAsks({ host, transport: getTransport() }) + + it("should get", async () => { + const index = await c.get() + expect(index).not.undefined + }) + + it("should query", async () => { + const q = new asksTypes.Query().toObject() + const res = await c.query(q) + expect(res).not.undefined + }) +}) diff --git a/src/asks/index.ts b/src/asks/index.ts new file mode 100644 index 00000000..58c58bd5 --- /dev/null +++ b/src/asks/index.ts @@ -0,0 +1,39 @@ +import { RPCServiceClient } from "@textile/grpc-powergate-client/dist/index/ask/rpc/rpc_pb_service" +import { asksTypes, Config } from "../types" +import { promise } from "../util" +import { queryObjectToMsg } from "./util" + +/** + * Creates the Asks API client + * @param config A config object that changes the behavior of the client + * @returns The Asks API client + */ +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const createAsks = (config: Config) => { + const client = new RPCServiceClient(config.host, config) + return { + /** + * Gets the asks index + * @returns The asks index + */ + get: () => + promise( + (cb) => client.get(new asksTypes.GetRequest(), cb), + (resp: asksTypes.GetResponse) => resp.toObject().index, + ), + + /** + * Queries the asks index + * @param query The query to run against the asks index + * @returns The asks matching the provided query + */ + query: (query: asksTypes.Query.AsObject) => { + const req = new asksTypes.QueryRequest() + req.setQuery(queryObjectToMsg(query)) + return promise( + (cb) => client.query(req, cb), + (resp: asksTypes.QueryResponse) => resp.toObject().asksList, + ) + }, + } +} diff --git a/src/asks/util.ts b/src/asks/util.ts new file mode 100644 index 00000000..a3d441ad --- /dev/null +++ b/src/asks/util.ts @@ -0,0 +1,10 @@ +import { asksTypes } from "../types" + +export function queryObjectToMsg(query: asksTypes.Query.AsObject): asksTypes.Query { + const ret = new asksTypes.Query() + ret.setLimit(query.limit) + ret.setMaxPrice(query.maxPrice) + ret.setOffset(query.offset) + ret.setPieceSize(query.pieceSize) + return ret +} diff --git a/src/faults/index.spec.ts b/src/faults/index.spec.ts new file mode 100644 index 00000000..11f0e87d --- /dev/null +++ b/src/faults/index.spec.ts @@ -0,0 +1,12 @@ +import { expect } from "chai" +import { createFaults } from "." +import { getTransport, host } from "../util" + +describe("faults", () => { + const c = createFaults({ host, transport: getTransport() }) + + it("should get", async () => { + const index = await c.get() + expect(index).not.undefined + }) +}) diff --git a/src/faults/index.ts b/src/faults/index.ts new file mode 100644 index 00000000..0818df17 --- /dev/null +++ b/src/faults/index.ts @@ -0,0 +1,24 @@ +import { RPCServiceClient } from "@textile/grpc-powergate-client/dist/index/faults/rpc/rpc_pb_service" +import { Config, faultsTypes } from "../types" +import { promise } from "../util" + +/** + * Creates the Faults API client + * @param config A config object that changes the behavior of the client + * @returns The Faults API client + */ +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const createFaults = (config: Config) => { + const client = new RPCServiceClient(config.host, config) + return { + /** + * Gets the faults index + * @returns The faults index + */ + get: () => + promise( + (cb) => client.get(new faultsTypes.GetRequest(), cb), + (resp: faultsTypes.GetResponse) => resp.toObject().index, + ), + } +} diff --git a/src/ffs/index.spec.ts b/src/ffs/index.spec.ts index 9def99aa..d09359eb 100644 --- a/src/ffs/index.spec.ts +++ b/src/ffs/index.spec.ts @@ -3,173 +3,191 @@ import fs from "fs" import { createFFS } from "." import { ffsTypes } from "../types" import { getTransport, host, useToken } from "../util" -import { withConfig, withHistory, withOverrideConfig } from "./options" +import { + PushConfigOption, + withConfig, + withHistory, + withIncludeFinal, + withOverrideConfig, +} from "./options" + +describe("ffs", function () { + this.timeout(180000) -describe("ffs", () => { const { getMeta, setToken } = useToken("") const c = createFFS({ host, transport: getTransport() }, getMeta) - let instanceId: string - let initialAddrs: ffsTypes.AddrInfo.AsObject[] - let defaultConfig: ffsTypes.DefaultConfig.AsObject - let cid: string - let defaultCidConfig: ffsTypes.CidConfig.AsObject - - it("should create an instance", async function () { - this.timeout(30000) - const res = await c.create() - expect(res.id).not.empty - expect(res.token).not.empty - instanceId = res.id - setToken(res.token) - // wait for 10 seconds so our wallet address gets funded - await new Promise((r) => setTimeout(r, 10000)) + it("should create an instance", async () => { + await expectNewInstance() }) it("should list instances", async () => { + await expectNewInstance() const res = await c.list() expect(res.instancesList).length.greaterThan(0) }) it("should get instance id", async () => { + const instanceInfo = await expectNewInstance() const res = await c.id() - expect(res.id).eq(instanceId) + expect(res.id).eq(instanceInfo.id) }) it("should get addrs", async () => { - const res = await c.addrs() - initialAddrs = res.addrsList - expect(initialAddrs).length.greaterThan(0) + await expectNewInstance() + await expectAddrs(1) }) it("should get the default config", async () => { - const res = await c.defaultConfig() - expect(res.defaultConfig).not.undefined - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - defaultConfig = res.defaultConfig! + await expectNewInstance() + await expectDefaultConfig() }) it("should create a new addr", async () => { - const res = await c.newAddr("my addr") - expect(res.addr).length.greaterThan(0) - const addrsRes = await c.addrs() - expect(addrsRes.addrsList).length(initialAddrs.length + 1) + await expectNewInstance() + await expectAddrs(1) + await expectNewAddr() + await expectAddrs(2) }) it("should set default config", async () => { + await expectNewInstance() + const defaultConfig = await expectDefaultConfig() await c.setDefaultConfig(defaultConfig) }) it("should get info", async () => { + await expectNewInstance() const res = await c.info() expect(res.info).not.undefined }) it("should add to hot", async () => { - const buffer = fs.readFileSync(`sample-data/samplefile`) - const res = await c.addToHot(buffer) - expect(res.cid).length.greaterThan(0) - cid = res.cid + await expectNewInstance() + await expectAddToHot("sample-data/samplefile") }) it("should get default cid config", async () => { - const res = await c.getDefaultCidConfig(cid) - expect(res.config?.cid).equal(cid) - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - defaultCidConfig = res.config! + await expectNewInstance() + const cid = await expectAddToHot("sample-data/samplefile") + await expectDefaultCidConfig(cid) }) - let jobId: string - it("should push config", async () => { - const res = await c.pushConfig(cid, withOverrideConfig(false), withConfig(defaultCidConfig)) - expect(res.jobId).length.greaterThan(0) - jobId = res.jobId - }) - - it("should watch job", function (done) { - this.timeout(180000) - const cancel = c.watchJobs((job) => { - expect(job.errCause).empty - expect(job.status).not.equal(ffsTypes.JobStatus.JOB_STATUS_CANCELED) - expect(job.status).not.equal(ffsTypes.JobStatus.JOB_STATUS_FAILED) - if (job.status === ffsTypes.JobStatus.JOB_STATUS_SUCCESS) { - cancel() - done() - } - }, jobId) + await expectNewInstance() + const cid = await expectAddToHot("sample-data/samplefile") + const config = await expectDefaultCidConfig(cid) + await expectPushConfig(cid, false, config) }) - it("should watch logs", function (done) { - this.timeout(10000) - const cancel = c.watchLogs( - (logEvent) => { - expect(logEvent.cid).not.empty - cancel() - done() - }, - cid, - withHistory(true), - ) + it("should watch job", async () => { + await expectNewInstance() + const addrs = await expectAddrs(1) + await waitForBalance(addrs[0].addr, 0) + const cid = await expectAddToHot("sample-data/samplefile") + const jobId = await expectPushConfig(cid) + await waitForJobStatus(jobId, ffsTypes.JobStatus.JOB_STATUS_SUCCESS) + }) + + it("should watch logs", async () => { + await expectNewInstance() + const cid = await expectAddToHot("sample-data/samplefile") + await expectPushConfig(cid) + await new Promise((resolve, reject) => { + const cancel = c.watchLogs( + (logEvent) => { + if (logEvent.cid.length > 0) { + cancel() + resolve() + } else { + cancel() + reject("empty log cid") + } + }, + cid, + withHistory(true), + ) + }) + }) + + it("should get a storage deal record", async () => { + await expectNewInstance() + const addrs = await expectAddrs(1) + await waitForBalance(addrs[0].addr, 0) + const cid = await expectAddToHot("sample-data/samplefile") + const jobId = await expectPushConfig(cid) + await waitForJobStatus(jobId, ffsTypes.JobStatus.JOB_STATUS_SUCCESS) + const res = await c.listStorageDealRecords(withIncludeFinal(true)) + expect(res).length.greaterThan(0) + }) + + it("should get a retrieval deal record", async () => { + // ToDo: Figure out hot to make sure the data in the previous push isn't cached in hot }) it("should get cid config", async () => { + await expectNewInstance() + const cid = await expectAddToHot("sample-data/samplefile") + await expectPushConfig(cid) const res = await c.getCidConfig(cid) expect(res.config?.cid).equal(cid) }) it("should show", async () => { + await expectNewInstance() + const addrs = await expectAddrs(1) + await waitForBalance(addrs[0].addr, 0) + const cid = await expectAddToHot("sample-data/samplefile") + const jobId = await expectPushConfig(cid) + await waitForJobStatus(jobId, ffsTypes.JobStatus.JOB_STATUS_SUCCESS) const res = await c.show(cid) expect(res.cidInfo).not.undefined }) it("should show all", async () => { + await expectNewInstance() + const addrs = await expectAddrs(1) + await waitForBalance(addrs[0].addr, 0) + const cid = await expectAddToHot("sample-data/samplefile") + const jobId = await expectPushConfig(cid) + await waitForJobStatus(jobId, ffsTypes.JobStatus.JOB_STATUS_SUCCESS) const res = await c.showAll() expect(res).not.empty }) - let buffer: Buffer - it("should replace", async () => { - buffer = fs.readFileSync(`sample-data/samplefile2`) - const res0 = await c.addToHot(buffer) - expect(res0.cid).length.greaterThan(0) - const res1 = await c.replace(cid, res0.cid) - expect(res1.jobId).length.greaterThan(0) - cid = res0.cid - jobId = res1.jobId - }) - - it("should watch replace job", function (done) { - this.timeout(180000) - const cancel = c.watchJobs((job) => { - expect(job.errCause).empty - expect(job.status).not.equal(ffsTypes.JobStatus.JOB_STATUS_CANCELED) - expect(job.status).not.equal(ffsTypes.JobStatus.JOB_STATUS_FAILED) - if (job.status === ffsTypes.JobStatus.JOB_STATUS_SUCCESS) { - cancel() - done() - } - }, jobId) + await expectNewInstance() + const addrs = await expectAddrs(1) + await waitForBalance(addrs[0].addr, 0) + const cid = await expectAddToHot("sample-data/samplefile") + const jobId = await expectPushConfig(cid) + await waitForJobStatus(jobId, ffsTypes.JobStatus.JOB_STATUS_SUCCESS) + const cid2 = await expectAddToHot("sample-data/samplefile2") + const res = await c.replace(cid, cid2) + expect(res.jobId).length.greaterThan(0) }) it("should get", async () => { + await expectNewInstance() + const addrs = await expectAddrs(1) + await waitForBalance(addrs[0].addr, 0) + const cid = await expectAddToHot("sample-data/samplefile") + const jobId = await expectPushConfig(cid) + await waitForJobStatus(jobId, ffsTypes.JobStatus.JOB_STATUS_SUCCESS) const bytes = await c.get(cid) - expect(bytes.byteLength).equal(buffer.byteLength) + expect(bytes.byteLength).greaterThan(0) }) it("should cancel a job", async () => { - buffer = fs.readFileSync(`sample-data/samplefile3`) - const res0 = await c.addToHot(buffer) - expect(res0.cid).length.greaterThan(0) - const res1 = await c.pushConfig(res0.cid) - expect(res1.jobId).length.greaterThan(0) - await c.cancelJob(res1.jobId) + await expectNewInstance() + const cid = await expectAddToHot("sample-data/samplefile") + const jobId = await expectPushConfig(cid) + await c.cancelJob(jobId) }) it("should list payment channels", async () => { - await c.listPayChannels() + // TODO }) it("should create a payment channel", async () => { @@ -180,8 +198,10 @@ describe("ffs", () => { // TODO }) - it("should push disable storage job", async () => { - const newConf: ffsTypes.CidConfig.AsObject = { + it("should remove", async () => { + await expectNewInstance() + const cid = await expectAddToHot("sample-data/samplefile") + const conf: ffsTypes.CidConfig.AsObject = { cid, repairable: false, cold: { @@ -192,35 +212,131 @@ describe("ffs", () => { enabled: false, }, } - const res0 = await c.pushConfig(cid, withOverrideConfig(true), withConfig(newConf)) - expect(res0).not.undefined - jobId = res0.jobId - }) - - it("should watch disable storage job", function (done) { - this.timeout(180000) - const cancel = c.watchJobs((job) => { - expect(job.errCause).empty - expect(job.status).not.equal(ffsTypes.JobStatus.JOB_STATUS_CANCELED) - expect(job.status).not.equal(ffsTypes.JobStatus.JOB_STATUS_FAILED) - if (job.status === ffsTypes.JobStatus.JOB_STATUS_SUCCESS) { - cancel() - done() - } - }, jobId) - }) - - it("should remove", async () => { + const jobId = await expectPushConfig(cid, false, conf) + await waitForJobStatus(jobId, ffsTypes.JobStatus.JOB_STATUS_SUCCESS) await c.remove(cid) }) it("should send fil", async () => { - const addrs = await c.addrs() - expect(addrs.addrsList).lengthOf(2) - await c.sendFil(addrs.addrsList[0].addr, addrs.addrsList[1].addr, 10) + await expectNewInstance() + const addrs = await expectAddrs(1) + await waitForBalance(addrs[0].addr, 0) + const addr = await expectNewAddr() + await c.sendFil(addrs[0].addr, addr, 10) }) it("should close", async () => { + await expectNewInstance() await c.close() }) + + async function expectNewInstance() { + const res = await c.create() + expect(res.id).not.empty + expect(res.token).not.empty + setToken(res.token) + return res + } + + async function expectAddrs(length: number) { + const res = await c.addrs() + expect(res.addrsList).length(length) + return res.addrsList + } + + async function expectNewAddr() { + const res = await c.newAddr("my addr") + expect(res.addr).length.greaterThan(0) + return res.addr + } + + async function expectDefaultConfig() { + const res = await c.defaultConfig() + expect(res.defaultConfig).not.undefined + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return res.defaultConfig! + } + + async function expectAddToHot(path: string) { + const buffer = fs.readFileSync(path) + const res = await c.addToHot(buffer) + expect(res.cid).length.greaterThan(0) + return res.cid + } + + async function expectDefaultCidConfig(cid: string) { + const res = await c.getDefaultCidConfig(cid) + expect(res.config).not.undefined + expect(res.config?.cid).equal(cid) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return res.config! + } + + async function expectPushConfig( + cid: string, + override: boolean = false, + config?: ffsTypes.CidConfig.AsObject, + ) { + const opts: PushConfigOption[] = [] + opts.push(withOverrideConfig(override)) + if (config) { + opts.push(withConfig(config)) + } + const res = await c.pushConfig(cid, ...opts) + expect(res.jobId).length.greaterThan(0) + return res.jobId + } + + function waitForJobStatus( + jobId: string, + status: ffsTypes.JobStatusMap[keyof ffsTypes.JobStatusMap], + ) { + return new Promise((resolve, reject) => { + try { + const cancel = c.watchJobs((job) => { + if (job.errCause.length > 0) { + reject(job.errCause) + } + if (job.status === ffsTypes.JobStatus.JOB_STATUS_CANCELED) { + reject("job canceled") + } + if (job.status === ffsTypes.JobStatus.JOB_STATUS_FAILED) { + reject("job failed") + } + if (job.status === status) { + cancel() + resolve() + } + }, jobId) + } catch (e) { + reject(e) + } + }) + } + + function waitForBalance(address: string, greaterThan: number) { + return new Promise(async (resolve, reject) => { + while (true) { + try { + const res = await c.info() + if (!res.info) { + reject("no balance info returned") + return + } + const info = res.info.balancesList.find((info) => info.addr?.addr === address) + if (!info) { + reject("address not in balances list") + return + } + if (info.balance > greaterThan) { + resolve(info.balance) + return + } + } catch (e) { + reject(e) + } + await new Promise((r) => setTimeout(r, 1000)) + } + }) + } }) diff --git a/src/ffs/index.ts b/src/ffs/index.ts index d35efaae..918da4f9 100644 --- a/src/ffs/index.ts +++ b/src/ffs/index.ts @@ -5,7 +5,7 @@ import { } from "@textile/grpc-powergate-client/dist/ffs/rpc/rpc_pb_service" import { Config, ffsTypes } from "../types" import { promise } from "../util" -import { PushConfigOption, WatchLogsOption } from "./options" +import { ListDealRecordsOption, PushConfigOption, WatchLogsOption } from "./options" import { coldObjToMessage, hotObjToMessage } from "./util" /** @@ -395,6 +395,42 @@ export const createFFS = (config: Config, getMeta: () => grpc.Metadata) => { ) }, + /** + * List storage deal records for the FFS instance according to the provided options + * @param opts Options that control the behavior of listing records + * @returns A list of storage deal records + */ + listStorageDealRecords: (...opts: ListDealRecordsOption[]) => { + const conf = new ffsTypes.ListDealRecordsConfig() + opts.forEach((opt) => { + opt(conf) + }) + const req = new ffsTypes.ListStorageDealRecordsRequest() + req.setConfig(conf) + return promise( + (cb) => client.listStorageDealRecords(req, getMeta(), cb), + (res: ffsTypes.ListStorageDealRecordsResponse) => res.toObject().recordsList, + ) + }, + + /** + * List retrieval deal records for the FFS instance according to the provided options + * @param opts Options that control the behavior of listing records + * @returns A list of retrieval deal records + */ + listRetrievalDealRecords: (...opts: ListDealRecordsOption[]) => { + const conf = new ffsTypes.ListDealRecordsConfig() + opts.forEach((opt) => { + opt(conf) + }) + const req = new ffsTypes.ListRetrievalDealRecordsRequest() + req.setConfig(conf) + return promise( + (cb) => client.listRetrievalDealRecords(req, getMeta(), cb), + (res: ffsTypes.ListRetrievalDealRecordsResponse) => res.toObject().recordsList, + ) + }, + /** * List cid infos for all data stored in the current FFS instance * @returns A list of cid info diff --git a/src/ffs/options.ts b/src/ffs/options.ts index 36f71472..c5b00b7c 100644 --- a/src/ffs/options.ts +++ b/src/ffs/options.ts @@ -8,12 +8,11 @@ export type PushConfigOption = (req: ffsTypes.PushConfigRequest) => void * @param override Whether or not to override any existing storage configuration * @returns The resulting option */ -export const withOverrideConfig = (override: boolean) => { - const option: PushConfigOption = (req: ffsTypes.PushConfigRequest) => { +export const withOverrideConfig = (override: boolean): PushConfigOption => { + return (req: ffsTypes.PushConfigRequest) => { req.setHasOverrideConfig(true) req.setOverrideConfig(override) } - return option } /** @@ -21,8 +20,8 @@ export const withOverrideConfig = (override: boolean) => { * @param config The storage configuration to use * @returns The resulting option */ -export const withConfig = (config: ffsTypes.CidConfig.AsObject) => { - const option: PushConfigOption = (req: ffsTypes.PushConfigRequest) => { +export const withConfig = (config: ffsTypes.CidConfig.AsObject): PushConfigOption => { + return (req: ffsTypes.PushConfigRequest) => { const c = new ffsTypes.CidConfig() c.setCid(config.cid) c.setRepairable(config.repairable) @@ -35,7 +34,6 @@ export const withConfig = (config: ffsTypes.CidConfig.AsObject) => { req.setHasConfig(true) req.setConfig(c) } - return option } export type WatchLogsOption = (res: ffsTypes.WatchLogsRequest) => void @@ -45,11 +43,10 @@ export type WatchLogsOption = (res: ffsTypes.WatchLogsRequest) => void * @param includeHistory Whether or not to include the history of log events * @returns The resulting option */ -export const withHistory = (includeHistory: boolean) => { - const option: WatchLogsOption = (req: ffsTypes.WatchLogsRequest) => { +export const withHistory = (includeHistory: boolean): WatchLogsOption => { + return (req: ffsTypes.WatchLogsRequest) => { req.setHistory(includeHistory) } - return option } /** @@ -57,9 +54,71 @@ export const withHistory = (includeHistory: boolean) => { * @param jobId The job id to show events for * @returns The resulting option */ -export const withJobId = (jobId: string) => { - const option: WatchLogsOption = (req: ffsTypes.WatchLogsRequest) => { +export const withJobId = (jobId: string): WatchLogsOption => { + return (req: ffsTypes.WatchLogsRequest) => { req.setJid(jobId) } - return option +} + +export type ListDealRecordsOption = (req: ffsTypes.ListDealRecordsConfig) => void + +/** + * Limits the results deals initiated from the provided wallet addresses + * @param addresses The list of addresses + * @returns The resulting option + */ +export const withFromAddresses = (...addresses: string[]): ListDealRecordsOption => { + return (req: ffsTypes.ListDealRecordsConfig) => { + req.setFromAddrsList(addresses) + } +} + +/** + * Limits the results to deals for the provided data cids + * @param cids The list of cids + * @returns The resulting option + */ +export const withDataCids = (...cids: string[]): ListDealRecordsOption => { + return (req: ffsTypes.ListDealRecordsConfig) => { + req.setDataCidsList(cids) + } +} + +/** + * Specifies whether or not to include pending deals in the results + * Default is false + * Ignored for listRetrievalDealRecords + * @param includePending Whether or not to include pending deal records + * @returns The resulting option + */ +export const withIncludePending = (includePending: boolean): ListDealRecordsOption => { + return (req: ffsTypes.ListDealRecordsConfig) => { + req.setIncludePending(includePending) + } +} + +/** + * Specifies whether or not to include final deals in the results + * Default is false + * Ignored for listRetrievalDealRecords + * @param includeFinal Whether or not to include final deal records + * @returns The resulting option + */ +export const withIncludeFinal = (includeFinal: boolean): ListDealRecordsOption => { + return (req: ffsTypes.ListDealRecordsConfig) => { + req.setIncludeFinal(includeFinal) + } +} + +/** + * Specifies to sort the results in ascending order + * Default is descending order + * Records are sorted by timestamp + * @param ascending Whether or not to sort the results in ascending order + * @returns The resulting option + */ +export const withAscending = (ascending: boolean): ListDealRecordsOption => { + return (req: ffsTypes.ListDealRecordsConfig) => { + req.setAscending(ascending) + } } diff --git a/src/ffs/util.ts b/src/ffs/util.ts index 8d371b75..88a5aca2 100644 --- a/src/ffs/util.ts +++ b/src/ffs/util.ts @@ -1,6 +1,6 @@ import { ffsTypes } from "../types" -export function coldObjToMessage(obj: ffsTypes.ColdConfig.AsObject) { +export function coldObjToMessage(obj: ffsTypes.ColdConfig.AsObject): ffsTypes.ColdConfig { const cold = new ffsTypes.ColdConfig() cold.setEnabled(obj.enabled) if (obj.filecoin) { @@ -23,7 +23,7 @@ export function coldObjToMessage(obj: ffsTypes.ColdConfig.AsObject) { return cold } -export function hotObjToMessage(obj: ffsTypes.HotConfig.AsObject) { +export function hotObjToMessage(obj: ffsTypes.HotConfig.AsObject): ffsTypes.HotConfig { const hot = new ffsTypes.HotConfig() hot.setAllowUnfreeze(obj.allowUnfreeze) hot.setEnabled(obj.enabled) diff --git a/src/index.spec.ts b/src/index.spec.ts index 9565d4ba..9178f592 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -4,8 +4,8 @@ import wait from "wait-on" import { createPow } from "." import { host } from "./util" -before(async function () { - this.timeout(130000) +beforeEach(async function () { + this.timeout(120000) cp.exec(`cd powergate-docker && BIGSECTORS=false make localnet`, (err) => { if (err) { throw err @@ -17,8 +17,16 @@ before(async function () { }) }) -after(() => { - cp.exec(`cd powergate-docker && make down`) +afterEach(async function () { + this.timeout(120000) + await new Promise((resolve, reject) => { + cp.exec(`cd powergate-docker && make down`, (err, stdout) => { + if (err) { + reject(err) + } + resolve(stdout) + }) + }) }) describe("client", () => { diff --git a/src/index.ts b/src/index.ts index 191ac8d2..2bf7795a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,37 @@ +import { createAsks } from "./asks" +import { createFaults } from "./faults" import { createFFS } from "./ffs" import { createHealth } from "./health" import { createMiners } from "./miners" import { createNet } from "./net" import { ffsOptions } from "./options" -import { Config, ffsTypes, healthTypes, minersTypes, netTypes } from "./types" +import { createReputation } from "./reputation" +import { + asksTypes, + Config, + faultsTypes, + ffsTypes, + healthTypes, + minersTypes, + netTypes, + reputationTypes, + walletTypes, +} from "./types" import { getTransport, host, useToken } from "./util" +import { createWallet } from "./wallet" -export { Config, ffsTypes, healthTypes, minersTypes, netTypes, ffsOptions } +export { ffsOptions } +export { + asksTypes, + Config, + faultsTypes, + ffsTypes, + healthTypes, + minersTypes, + netTypes, + reputationTypes, + walletTypes, +} const defaultConfig: Config = { host, @@ -18,6 +43,7 @@ const defaultConfig: Config = { * @param config A config object that changes the behavior of the client * @returns A Powergate client API */ +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export const createPow = (config?: Partial) => { const c = { ...defaultConfig, ...removeEmpty(config) } @@ -31,24 +57,44 @@ export const createPow = (config?: Partial) => { setToken, /** - * The Health API + * The Asks API */ - health: createHealth(c), + asks: createAsks(c), /** - * The Net API + * The Faults API */ - net: createNet(c), + faults: createFaults(c), /** * The FFS API */ ffs: createFFS(c, getMeta), + /** + * The Health API + */ + health: createHealth(c), + /** * The Miners API */ miners: createMiners(c), + + /** + * The Net API + */ + net: createNet(c), + + /** + * The Reputation API + */ + reputation: createReputation(c), + + /** + * The Wallet API + */ + wallet: createWallet(c), } } diff --git a/src/net/index.spec.ts b/src/net/index.spec.ts index 2eb62279..5837cc70 100644 --- a/src/net/index.spec.ts +++ b/src/net/index.spec.ts @@ -1,16 +1,14 @@ -import { Connectedness, PeersResponse } from "@textile/grpc-powergate-client/dist/net/rpc/rpc_pb" +import { Connectedness } from "@textile/grpc-powergate-client/dist/net/rpc/rpc_pb" import { assert, expect } from "chai" import { createNet } from "." +import { netTypes } from "../types" import { getTransport, host } from "../util" describe("net", () => { const c = createNet({ host, transport: getTransport() }) - let peers: PeersResponse.AsObject - it("should query peers", async () => { - peers = await c.peers() - expect(peers.peersList).length.greaterThan(0) + await expectPeers() }) it("should get listen address", async () => { @@ -20,29 +18,37 @@ describe("net", () => { }) it("should find a peer", async () => { - const peerId = peers.peersList[0].addrInfo?.id - if (!peerId) { - assert.fail("no peer id") - } + const peers = await expectPeers() + const peerId = expectPeerInfo(peers).id const peer = await c.findPeer(peerId) expect(peer.peerInfo).not.undefined }) it("should get peer connectedness", async () => { - const peerId = peers.peersList[0].addrInfo?.id - if (!peerId) { - assert.fail("no peer id") - } + const peers = await expectPeers() + const peerId = expectPeerInfo(peers).id const resp = await c.connectedness(peerId) expect(resp.connectedness).equal(Connectedness.CONNECTEDNESS_CONNECTED) }) it("should disconnect and reconnect to a peer", async () => { - const peerInfo = peers.peersList[0].addrInfo - if (!peerInfo) { - assert.fail("no peer info") - } + const peers = await expectPeers() + const peerInfo = expectPeerInfo(peers) await c.disconnectPeer(peerInfo.id) await c.connectPeer(peerInfo) }) + + async function expectPeers() { + const peers = await c.peers() + expect(peers.peersList).length.greaterThan(0) + return peers + } + + function expectPeerInfo(peersResp: netTypes.PeersResponse.AsObject) { + const peerInfo = peersResp.peersList[0].addrInfo + if (!peerInfo) { + assert.fail("no peer info") + } + return peerInfo + } }) diff --git a/src/reputation/index.spec.ts b/src/reputation/index.spec.ts new file mode 100644 index 00000000..eee059e1 --- /dev/null +++ b/src/reputation/index.spec.ts @@ -0,0 +1,12 @@ +import { expect } from "chai" +import { createReputation } from "." +import { getTransport, host } from "../util" + +describe("reputation", () => { + const c = createReputation({ host, transport: getTransport() }) + + it("should get top miners", async () => { + const scores = await c.getTopMiners(10) + expect(scores).not.undefined + }) +}) diff --git a/src/reputation/index.ts b/src/reputation/index.ts new file mode 100644 index 00000000..470d5d94 --- /dev/null +++ b/src/reputation/index.ts @@ -0,0 +1,45 @@ +import { RPCServiceClient } from "@textile/grpc-powergate-client/dist/reputation/rpc/rpc_pb_service" +import { Config, reputationTypes } from "../types" +import { promise } from "../util" + +/** + * Creates the Reputation API client + * @param config A config object that changes the behavior of the client + * @returns The Reputation API client + */ +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const createReputation = (config: Config) => { + const client = new RPCServiceClient(config.host, config) + return { + /** + * Adds a data source to the reputation index + * @param id The id of the data source + * @param multiaddress The multiaddress of the data source + */ + addSource: (id: string, multiaddress: string) => { + const req = new reputationTypes.AddSourceRequest() + req.setId(id) + req.setMaddr(multiaddress) + return promise( + (cb) => client.addSource(req, cb), + () => { + // nothing to return + }, + ) + }, + + /** + * Gets the top ranked miners + * @param limit Limits the number of results + * @returns The list of miner scores + */ + getTopMiners: (limit: number) => { + const req = new reputationTypes.GetTopMinersRequest() + req.setLimit(limit) + return promise( + (cb) => client.getTopMiners(req, cb), + (resp: reputationTypes.GetTopMinersResponse) => resp.toObject().topMinersList, + ) + }, + } +} diff --git a/src/types.ts b/src/types.ts index 0a565994..81598aee 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,8 +1,12 @@ import { grpc } from "@improbable-eng/grpc-web" import * as ffsTypes from "@textile/grpc-powergate-client/dist/ffs/rpc/rpc_pb" import * as healthTypes from "@textile/grpc-powergate-client/dist/health/rpc/rpc_pb" +import * as asksTypes from "@textile/grpc-powergate-client/dist/index/ask/rpc/rpc_pb" +import * as faultsTypes from "@textile/grpc-powergate-client/dist/index/faults/rpc/rpc_pb" import * as minersTypes from "@textile/grpc-powergate-client/dist/index/miner/rpc/rpc_pb" import * as netTypes from "@textile/grpc-powergate-client/dist/net/rpc/rpc_pb" +import * as reputationTypes from "@textile/grpc-powergate-client/dist/reputation/rpc/rpc_pb" +import * as walletTypes from "@textile/grpc-powergate-client/dist/wallet/rpc/rpc_pb" /** * Object that allows you to configure the Powergate client @@ -12,4 +16,13 @@ export interface Config extends grpc.RpcOptions { authToken?: string } -export { ffsTypes, healthTypes, minersTypes, netTypes } +export { + ffsTypes, + healthTypes, + minersTypes, + netTypes, + asksTypes, + faultsTypes, + reputationTypes, + walletTypes, +} diff --git a/src/wallet/index.spec.ts b/src/wallet/index.spec.ts new file mode 100644 index 00000000..12e97b46 --- /dev/null +++ b/src/wallet/index.spec.ts @@ -0,0 +1,38 @@ +import { expect } from "chai" +import { createWallet } from "." +import { getTransport, host } from "../util" + +describe("wallet", () => { + const c = createWallet({ host, transport: getTransport() }) + + it("should list addresses", async () => { + await expectAddresses(0) + }) + + it("should create a new address", async () => { + await expectNewAddress() + }) + + it("should check balance", async () => { + const addrs = await expectAddresses(0) + await c.balance(addrs[0]) + }) + + it("should send fil", async () => { + const addrs = await expectAddresses(0) + const newAddr = await expectNewAddress() + await c.sendFil(addrs[0], newAddr, 10) + }) + + async function expectAddresses(lengthGreaterThan: number) { + const addresses = await c.list() + expect(addresses).length.greaterThan(lengthGreaterThan) + return addresses + } + + async function expectNewAddress() { + const address = await c.newAddress() + expect(address).length.greaterThan(0) + return address + } +}) diff --git a/src/wallet/index.ts b/src/wallet/index.ts new file mode 100644 index 00000000..235b68fe --- /dev/null +++ b/src/wallet/index.ts @@ -0,0 +1,71 @@ +import { RPCServiceClient } from "@textile/grpc-powergate-client/dist/wallet/rpc/rpc_pb_service" +import { Config, walletTypes } from "../types" +import { promise } from "../util" + +/** + * Creates the Wallet API client + * @param config A config object that changes the behavior of the client + * @returns The Wallet API client + */ +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export const createWallet = (config: Config) => { + const client = new RPCServiceClient(config.host, config) + return { + /** + * Create a new wallet address + * @param type The type of address to create, bls or secp256k1 + * @returns The new address + */ + newAddress: (type: "bls" | "secp256k1" = "bls") => { + const req = new walletTypes.NewAddressRequest() + req.setType(type) + return promise( + (cb) => client.newAddress(req, cb), + (resp: walletTypes.NewAddressResponse) => resp.toObject().address, + ) + }, + + /** + * List all wallet addresses + * @returns The list of wallet addresses + */ + list: () => + promise( + (cb) => client.list(new walletTypes.ListRequest(), cb), + (resp: walletTypes.ListResponse) => resp.toObject().addressesList, + ), + + /** + * Get the balance for a wallet address + * @param address The address to get the balance for + * @returns The address balance + */ + balance: (address: string) => { + const req = new walletTypes.BalanceRequest() + req.setAddress(address) + return promise( + (cb) => client.balance(req, cb), + (resp: walletTypes.BalanceResponse) => resp.toObject().balance, + ) + }, + + /** + * Send Fil from one address to another + * @param from The address to send from + * @param to The address to send to + * @param amount The amount of Fil to send + */ + sendFil: (from: string, to: string, amount: number) => { + const req = new walletTypes.SendFilRequest() + req.setFrom(from) + req.setTo(to) + req.setAmount(amount) + return promise( + (cb) => client.sendFil(req, cb), + () => { + // nothing to return + }, + ) + }, + } +}