Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/ao/utils/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SendMessage } from "@permaweb/aoconnect/dist/lib/message";
import { ReadResult } from "@permaweb/aoconnect/dist/lib/result";
import { Types as AoConnectTypes } from "@permaweb/aoconnect/dist/dal";
import * as aoConnect from "@permaweb/aoconnect";
import { ReadResults } from "@permaweb/aoconnect/dist/lib/results";

const DEFAULT_SERVICES: Services = {
MODE: "legacy",
Expand Down
222 changes: 222 additions & 0 deletions src/functions/utils/trackResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import { Tag } from "../../arweave/getTags";
import { AoUtils, connectToAO } from "../../ao/utils/connect";

export interface TrackResult {
process: string;
message: string;
targetProcess?: string;
messageTimestamp?: number;
validUntil?: number;
validateOriginal?: boolean;
match: {
success?: ResultMatcher;
fail?: ResultMatcher;
};
}

export interface TrackResultRes {
match: "success" | "fail";
message: PlainMessage;
}

export interface SimpleTag {
name: string;
value: string;
}

export interface PlainMessage {
Anchor: string;
Tags: SimpleTag[];
Target: string;
Data: string;
}

export interface ResultMatcher extends Omit<Partial<PlainMessage>, "Tags"> {
Tags?: Tag[];
}

const SU_ROUTER = "https://su-router.ao-testnet.xyz";

export async function trackResult(
aoUtilsInput: Pick<AoUtils, "signer" | "configs">,
{
process,
message,
targetProcess,
match,
messageTimestamp,
validateOriginal = true,
validUntil = 1000 * 60 * 45
}: TrackResult
): Promise<TrackResultRes | undefined> {
if (!process || !message) {
throw new Error("Please specify a process and a message id");
}
if (!match.success && !match.fail) {
throw new Error("Please specify an expected success/fail result match");
}

const aoUtils = await connectToAO(aoUtilsInput.configs);

// check if a tag matches a result matcher
const matchTag = (tag: SimpleTag, expected: Tag) => {
if (tag.name !== expected.name) return false;
if (typeof expected.values !== "string") {
return expected.values.includes(tag.value);
}
return tag.value === expected.values;
};

// check if an expected message matches another
const matchMsg = (msg: PlainMessage, expected: ResultMatcher) =>
(!expected.Anchor || expected.Anchor === msg.Anchor) &&
(!expected.Data || expected.Data === msg.Data) &&
(!expected.Target || expected.Target === msg.Target) &&
(!expected.Tags || expected.Tags.every((tag1) => msg.Tags.find(
(tag2) => matchTag(tag2, tag1)
)));

// the returned result
let matchedResult: TrackResultRes | undefined;

// validate input message result
if (validateOriginal) {
const res = await aoUtils.result({ process, message });

for (const msg of res.Messages as PlainMessage[]) {
if (match.success && matchMsg(msg, match.success)) {
matchedResult = {
match: "success",
message: msg
};
break;
} else if (match.fail && matchMsg(msg, match.fail)) {
matchedResult = {
match: "fail",
message: msg
};
break;
}
}
}

// early return if the original message produced the
// expected result (most likely an error, in case of
// transfer invoked messages)
if (matchedResult) {
return matchedResult;
}

// first get scheduled message data
if (!messageTimestamp) {
const res = await fetch(`${SU_ROUTER}/${message}?process-id=${process}`);
if (res.status >= 400) {
throw new Error(`Could not find message ${message} on process ${process}`);
}

// parse timestamp from message data
const messageData: ScheduledMessage = await res.json();
messageTimestamp = parseInt(messageData.assignment.tags.find(
(tag) => tag.name === "Timestamp"
)?.value as string);

if (!messageTimestamp || Number.isNaN(messageTimestamp)) {
throw new Error(`Could not parse message timestamp for ${message}`);
}
}

// find the final message pushed for this message on the SU
const resultProcess = targetProcess || process;
const untilTimestamp = messageTimestamp + validUntil + 1;
let iterateNextPage = true;
let cursor = messageTimestamp - 1;

while (!matchedResult && iterateNextPage && cursor <= untilTimestamp) {
const res: MessagesList = await (
await fetch(
`${SU_ROUTER}/${resultProcess}?from=${cursor}&to=${untilTimestamp}`
)
).json();
const potentialResultMessages: MessageOrAssignment[] = [];

for (const interaction of res.edges) {
const { message: msg } = interaction.node;

// check if the iterated message was pushed for the original message.
// if it was, we store it to read it's result later
if (msg.tags.find((tag) => tag.name === "Pushed-For")?.value === message) {
potentialResultMessages.push(msg);
}

cursor = parseInt(interaction.cursor);
}

// now we read the result for all of the potential closing messages
if (potentialResultMessages.length > 0) {
potentialResultMessages.reverse();

for (const generatingMsg of potentialResultMessages) {
const msgResult = await aoUtils.result({
process: resultProcess,
message: generatingMsg.id
});

for (const msg of msgResult.Messages as PlainMessage[]) {
if (match.success && matchMsg(msg, match.success)) {
matchedResult = {
match: "success",
message: msg
};
break;
} else if (match.fail && matchMsg(msg, match.fail)) {
matchedResult = {
match: "fail",
message: msg
};
break;
}
}

if (matchedResult) break;
}
}

iterateNextPage = res.page_info.has_next_page;
}

return matchedResult;
}

interface MessageOrAssignment {
id: string;
owner: {
address: string;
key: string;
};
data: string;
tags: {
name: string;
value: string;
}[];
signature: string;
target?: string | "";
anchor?: string;
}

interface ScheduledMessage {
message: MessageOrAssignment;
assignment: MessageOrAssignment;
}

interface MessagesList {
page_info: {
has_next_page: false;
};
edges: {
node: {
message: MessageOrAssignment;
assignment: MessageOrAssignment;
};
cursor: string;
}[];
}
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ import { connectToAO, AoUtils } from "./ao/utils/connect";
import { Services } from "./ao/utils/connect";
type Configs = Services;
import { Types as AoConnectTypes } from "@permaweb/aoconnect/dist/dal";
import { trackResult, TrackResult, TrackResultRes } from "./functions/utils/trackResult";
import {
getEarnings,
GetEarnings,
Expand Down Expand Up @@ -249,6 +250,10 @@ class LiquidOps {
return transfer({ signer: this.signer, configs: this.configs }, params);
}

async trackResult(params: TrackResult): Promise<TrackResultRes | undefined> {
return trackResult({ signer: this.signer, configs: this.configs }, params);
}

//--------------------------------------------------------------------------------------------------------------- process data

static oTokens = oTokens;
Expand Down
22 changes: 22 additions & 0 deletions tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,25 @@ const walletAddress = await ownerToAddress(JWK.n);
// console.log(transfer)

//-------------------------------

// const result = await client.trackResult({
// process: "7zH9dlMNoxprab9loshv3Y7WG45DOny_Vrq9KrXObdQ",
// message: "bHjeHOic0GSOp4jMB9DbHs6ZXSWjCSNi_2iYTYjiKeg",
// targetProcess: "4MW7uLFtttSLWM-yWEqV9TGD6fSIDrqa4lbTgYL2qHg",
// match: {
// success: {
// Target: "ljvCPN31XCLPkBo9FUeB7vAK0VC6-eY52-CS-6Iho8U",
// Tags: [
// { name: "Action", values: "Mint-Confirmation" }
// ]
// },
// fail: {
// Target: "ljvCPN31XCLPkBo9FUeB7vAK0VC6-eY52-CS-6Iho8U",
// Tags: [
// { name: "Action", values: "Mint-Error" }
// ]
// }
// }
// });

// console.log(result);
Loading