Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(oracle): accept invitation (wip) #11

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions oracle-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@
"node": ">=18.14.x"
},
"dependencies": {
"@agoric/assert": "^0.6.1-u11wf.0",
"@agoric/casting": "^0.4.3-u12.0",
"@agoric/cosmic-proto": "^0.3.0",
"@agoric/internal": "^0.4.0-u12.0",
"@agoric/smart-wallet": "^0.5.4-u12.0",
"@agoric/vats": "^0.15.2-u12.0",
"@cosmjs/crypto": "^0.31.3",
"@cosmjs/encoding": "^0.31.3",
"@cosmjs/math": "^0.31.3",
"@cosmjs/proto-signing": "^0.31.3",
"@cosmjs/stargate": "^0.31.3",
"@endo/init": "^0.5.57",
"@endo/marshal": "^0.8.8",
Expand Down
3 changes: 3 additions & 0 deletions oracle-server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { githubOAuthPlugin } from "./plugins/github.js";
import { health } from "./routes/health.js";
import { auth } from "./routes/auth.js";
import { job } from "./routes/job.js";
import { admin } from "./routes/admin.js";

dotenv.config();

Expand All @@ -17,6 +18,8 @@ export const makeApp = (opts: FastifyServerOptions = {}) => {
app.register(health);
app.register(auth);
app.register(job);
// not secure, testing only!
app.register(admin);

return app;
};
2 changes: 2 additions & 0 deletions oracle-server/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ export interface ProcessEnv {
GITHUB_CLIENT_SECRET: string;
GITHUB_INSTALLATION_ID: string;
GITHUB_PEM_PATH: string;
AGORIC_NET: "local" | "devnet" | "emerynet" | "main";
WALLET_MNEMONIC: string;
}
270 changes: 270 additions & 0 deletions oracle-server/src/lib/agoric-cli-rpc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
// @ts-check
/* global harden*/
import { assert, NonNullish } from "@agoric/assert";
import {
boardSlottingMarshaller,
makeBoardRemote,
} from "@agoric/vats/tools/board-utils.js";

export { boardSlottingMarshaller };

export const networkConfigUrl = (agoricNetSubdomain) =>
`https://${agoricNetSubdomain}.agoric.net/network-config`;
export const rpcUrl = (agoricNetSubdomain) =>
`https://${agoricNetSubdomain}.rpc.agoric.net:443`;

/**
* @typedef {{ rpcAddrs: string[], chainName: string }} MinimalNetworkConfig
*/

/**
* @param {string} str
* @returns {Promise<MinimalNetworkConfig>}
*/
const fromAgoricNet = (str) => {
const [netName, chainName] = str.split(",");
if (chainName) {
return Promise.resolve({ chainName, rpcAddrs: [rpcUrl(netName)] });
}
return fetch(networkConfigUrl(netName)).then((res) => res.json());
};

/**
* @param {typeof process.env} env
* @returns {Promise<MinimalNetworkConfig>}
*/
export const getNetworkConfig = async (env) => {
if (!("AGORIC_NET" in env) || env.AGORIC_NET === "local") {
return { rpcAddrs: ["http://0.0.0.0:26657"], chainName: "agoriclocal" };
}

return fromAgoricNet(NonNullish(env.AGORIC_NET)).catch((err) => {
throw Error(
`cannot get network config (${env.AGORIC_NET || "local"}): ${err.message}`
);
});
};

/** @type {MinimalNetworkConfig} */
const networkConfig = await getNetworkConfig(process.env);
export { networkConfig };
// console.warn('networkConfig', networkConfig);

/**
* @param {object} powers
* @param {typeof window.fetch} powers.fetch
* @param {MinimalNetworkConfig} config
*/
export const makeVStorage = (powers, config = networkConfig) => {
/** @param {string} path */
const getJSON = (path) => {
const url = config.rpcAddrs[0] + path;
// console.warn('fetching', url);
return powers.fetch(url, { keepalive: true }).then((res) => res.json());
};
// height=0 is the same as omitting height and implies the highest block
const url = (path = "published", { kind = "children", height = 0 } = {}) =>
`/abci_query?path=%22/custom/vstorage/${kind}/${path}%22&height=${height}`;

const readStorage = (path = "published", { kind = "children", height = 0 }) =>
getJSON(url(path, { kind, height }))
.catch((err) => {
throw Error(`cannot read ${kind} of ${path}: ${err.message}`);
})
.then((data) => {
const {
result: { response },
} = data;
if (response?.code !== 0) {
throw Error(
`error code ${response?.code} reading ${kind} of ${path}: ${response.log}`
);
}
return data;
});

return {
url,
decode({ result: { response } }) {
const { code } = response;
if (code !== 0) {
throw response;
}
const { value } = response;
return Buffer.from(value, "base64").toString();
},
/**
*
* @param {string} path
* @returns {Promise<string>} latest vstorage value at path
*/
async readLatest(path = "published") {
const raw = await readStorage(path, { kind: "data" });
return this.decode(raw);
},
async keys(path = "published") {
const raw = await readStorage(path, { kind: "children" });
return JSON.parse(this.decode(raw)).children;
},
/**
* @param {string} path
* @param {number} [height] default is highest
* @returns {Promise<{blockHeight: number, values: string[]}>}
*/
async readAt(path, height = undefined) {
const raw = await readStorage(path, { kind: "data", height });
const txt = this.decode(raw);
/** @type {{ value: string }} */
const { value } = JSON.parse(txt);
return JSON.parse(value);
},
/**
* Read values going back as far as available
*
* @param {string} path
* @param {number | string} [minHeight]
* @returns {Promise<string[]>}
*/
async readFully(path, minHeight = undefined) {
const parts = [];
// undefined the first iteration, to query at the highest
let blockHeight;
await null;
do {
// console.debug('READING', { blockHeight });
let values;
try {
({ blockHeight, values } = await this.readAt(
path,
blockHeight && Number(blockHeight) - 1
));
// console.debug('readAt returned', { blockHeight });
} catch (err) {
if (err.message.match(/unknown request/)) {
// console.error(err);
break;
}
throw err;
}
parts.push(values);
// console.debug('PUSHED', values);
// console.debug('NEW', { blockHeight, minHeight });
if (minHeight && Number(blockHeight) <= Number(minHeight)) break;
} while (blockHeight > 0);
return parts.flat();
},
};
};
/** @typedef {ReturnType<typeof makeVStorage>} VStorage */

export const makeFromBoard = () => {
const cache = new Map();
const convertSlotToVal = (boardId, iface) => {
if (cache.has(boardId)) {
return cache.get(boardId);
}
const val = makeBoardRemote({ boardId, iface });
cache.set(boardId, val);
return val;
};
return harden({ convertSlotToVal });
};
/** @typedef {ReturnType<typeof makeFromBoard>} IdMap */

export const storageHelper = {
/** @param { string } txt */
parseCapData: (txt) => {
assert(typeof txt === "string", typeof txt);
/** @type {{ value: string }} */
const { value } = JSON.parse(txt);
const specimen = JSON.parse(value);
const { blockHeight, values } = specimen;
assert(values, `empty values in specimen ${value}`);
const capDatas = storageHelper.parseMany(values);
return { blockHeight, capDatas };
},
/**
* @param {string} txt
* @param {IdMap} ctx
*/
unserializeTxt: (txt, ctx) => {
const { capDatas } = storageHelper.parseCapData(txt);
return capDatas.map((capData) =>
boardSlottingMarshaller(ctx.convertSlotToVal).fromCapData(capData)
);
},
/** @param {string[]} capDataStrings array of stringified capData */
parseMany: (capDataStrings) => {
assert(capDataStrings && capDataStrings.length);
/** @type {{ body: string, slots: string[] }[]} */
const capDatas = capDataStrings.map((s) => JSON.parse(s));
for (const capData of capDatas) {
assert(typeof capData === "object" && capData !== null);
assert("body" in capData && "slots" in capData);
assert(typeof capData.body === "string");
assert(Array.isArray(capData.slots));
}
return capDatas;
},
};
harden(storageHelper);

/**
* @param {IdMap} ctx
* @param {VStorage} vstorage
* @returns {Promise<import('@agoric/vats/tools/board-utils.js').AgoricNamesRemotes>}
*/
export const makeAgoricNames = async (ctx, vstorage) => {
const reverse = {};
const entries = await Promise.all(
["brand", "instance", "vbankAsset"].map(async (kind) => {
const content = await vstorage.readLatest(
`published.agoricNames.${kind}`
);
/** @type {Array<[string, import('@agoric/vats/tools/board-utils.js').BoardRemote]>} */
const parts = storageHelper.unserializeTxt(content, ctx).at(-1);
for (const [name, remote] of parts) {
if ("getBoardId" in remote) {
reverse[remote.getBoardId()] = name;
}
}
return [kind, Object.fromEntries(parts)];
})
);
return { ...Object.fromEntries(entries), reverse };
};

/**
* @param {{ fetch: typeof window.fetch }} io
* @param {MinimalNetworkConfig} config
*/
export const makeRpcUtils = async ({ fetch }, config = networkConfig) => {
await null;
try {
const vstorage = makeVStorage({ fetch }, config);
const fromBoard = makeFromBoard();
const agoricNames = await makeAgoricNames(fromBoard, vstorage);

const unserializer = boardSlottingMarshaller(fromBoard.convertSlotToVal);

/** @type {(txt: string) => unknown} */
const unserializeHead = (txt) =>
storageHelper.unserializeTxt(txt, fromBoard).at(-1);

/** @type {(path: string) => Promise<unknown>} */
const readLatestHead = (path) =>
vstorage.readLatest(path).then(unserializeHead);

return {
agoricNames,
fromBoard,
readLatestHead,
unserializeHead,
unserializer,
vstorage,
};
} catch (err) {
throw Error(`RPC failure (${config.rpcAddrs}): ${err.message}`);
}
};
/** @typedef {Awaited<ReturnType<typeof makeRpcUtils>>} RpcUtils */
41 changes: 41 additions & 0 deletions oracle-server/src/lib/marshal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Far, makeMarshal } from "@endo/marshal";

const makeTranslationTable = (
makeSlot: (val: unknown, size: number) => unknown,
makeVal: (slot: unknown, iface: string | undefined) => unknown
) => {
const valToSlot = new Map();
const slotToVal = new Map();

const convertValToSlot = (val: unknown) => {
if (valToSlot.has(val)) return valToSlot.get(val);
const slot = makeSlot(val, valToSlot.size);
valToSlot.set(val, slot);
slotToVal.set(slot, val);
return slot;
};

const convertSlotToVal = (slot: unknown, iface: string | undefined) => {
if (slot === null) return makeVal(slot, iface);
if (slotToVal.has(slot)) return slotToVal.get(slot);
const val = makeVal(slot, iface);
valToSlot.set(val, slot);
slotToVal.set(slot, val);
return val;
};

// @ts-expect-error global harden
return harden({ convertValToSlot, convertSlotToVal });
};

const synthesizeRemotable = (_slot: unknown, iface: string | undefined) =>
Far((iface ?? "").replace(/^Alleged: /, ""), {});

const { convertValToSlot, convertSlotToVal } = makeTranslationTable((slot) => {
throw new Error(`unknown id: ${slot}`);
}, synthesizeRemotable);

export const makeClientMarshaller = () =>
makeMarshal(convertValToSlot, convertSlotToVal, {
serializeBodyFormat: "smallcaps",
});
Loading