Skip to content

Commit

Permalink
api: add more currencies, better uniswap cache (#1072)
Browse files Browse the repository at this point in the history
* api: add more curerncies

* api: upgrade Uniswap cache

* api: add getUniswapRoute for debugging

* clippy: get-user, get-uniswap
  • Loading branch information
dcposch authored May 25, 2024
1 parent f0db31f commit 817e1da
Show file tree
Hide file tree
Showing 13 changed files with 321 additions and 67 deletions.
2 changes: 1 addition & 1 deletion apps/daimo-clippy/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"version": "1.0.0",
"main": "src/index.ts",
"scripts": {
"dev": "tsx watch src/index.ts",
"dev": "echo Use 'npm start' for Clippy dev",
"start": "tsx src/index.ts",
"lint": "npm run lint:deps && npm run lint:style",
"lint:deps": "npx depcheck --ignores @tsconfig/node20,@types/tape,ts-node",
Expand Down
130 changes: 122 additions & 8 deletions apps/daimo-clippy/src/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { assertNotNull, formatDaimoLink, parseDaimoLink } from "@daimo/common";
import { Address, getAddress } from "viem";
import {
EAccount,
amountToDollars,
assertNotNull,
dollarsToAmount,
formatDaimoLink,
getSlotLabel,
parseDaimoLink,
} from "@daimo/common";
import { Address, getAddress, isAddress } from "viem";

import { rpc } from "./rpc";
import { getJSONblock, parseKwargs, unfurlLink } from "./utils";
Expand All @@ -22,13 +30,13 @@ export async function handleCommand(text: string): Promise<string> {

// Check all remaining args are kwargs.
if (args.slice(2).some((a) => !a.includes("=")))
return help(`Invalid args ${text}`);
return help(`Invalid args ${text}`, args[1]);

try {
return await command.fn(parseKwargs(args.slice(2)));
} catch (e) {
console.error(`[SLACK-BOT] Error handling command: ${e}`);
return help(`Error handling command ${text}: ${e}`);
return help(`Error handling command ${text}: ${e}`, args[1]);
}
}

Expand Down Expand Up @@ -56,12 +64,114 @@ const commands: Record<string, Command> = {
help: "Set max uses. Args: [link, max_uses]",
fn: setMaxUses,
},
"get-user": {
help: "Gets name, address and balance of a user. Args: [user = name or addr]",
fn: getUser,
},
"get-uniswap": {
help: "Gets the best Uniswap route for a given token to USDC. Args: [num=1.23, token=DAI]",
fn: getUniswapRoute,
},
health: {
help: "Checks that the API is up",
fn: health,
},
help: {
help: "Show this help message",
fn: (_) => help(),
},
};

async function health(): Promise<string> {
const health = await rpc.health.query();
return JSON.stringify(health);
}

async function getUser(kwargs: Map<string, string>): Promise<string> {
const user = kwargs.get("user");
if (!user) throw new Error("Must specify user");

let address: Address;
let eAcc: EAccount;
if (isAddress(user)) {
address = getAddress(user);
eAcc = await rpc.getEthereumAccount.query({ addr: address });
} else {
const addr = await rpc.resolveName.query({ name: user });
if (addr == null) return `User '${user}' not found`;
eAcc = { addr, name: user };
address = addr;
}

if (eAcc.name == null) {
console.log(`Not a Daimo account: ${JSON.stringify(eAcc)}`);
}

const hist = await rpc.getAccountHistory.query({ address, sinceBlockNum: 0 });

return [
`Name : ${eAcc.name}`,
`Address : ${address}`,
`Balance : ${amountToDollars(BigInt(hist.lastBalance))} USDC`,
`Keys : ${hist.accountKeys
.map((k) => getSlotLabel(k.slot))
.join(", ")}`,
`Linked Accts: ${hist.linkedAccounts
.map((a) => `${a.type} ${a.username}`)
.join(", ")}`,
`# swaps : ${hist.proposedSwaps.length}`,
`# transfers : ${hist.transferLogs.length}`,
].join("\n");
}

type TokenList = {
tokens: {
chainId: number;
address: Address;
name: string;
symbol: string;
decimals: number;
logoURI?: string;
}[];
version: any;
};
let tokenListPromise: Promise<TokenList> | null = null;

async function getTokenList(): Promise<TokenList> {
if (tokenListPromise == null) {
tokenListPromise = fetch("https://tokens.coingecko.com/base/all.json").then(
(a) => a.json()
);
}
return tokenListPromise;
}

async function getUniswapRoute(kwargs: Map<string, string>): Promise<string> {
const strN = kwargs.get("num");
const strToken = kwargs.get("token");
if (!strN || !strToken) return "Must specify num and token";

const { tokens } = await getTokenList();
const token = tokens.find(
(t) => t.symbol === strToken || t.address === strToken.toLowerCase()
);
if (token == null) return `Token '${strToken}' not found`;

const fromAmount = dollarsToAmount(Number(strN), token.decimals);

const route = await rpc.getUniswapRoute.query({
fromToken: getAddress(token.address),
fromAmount: "" + fromAmount,
toAddr: getAddress("0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead"),
});

const fromStr = `${fromAmount} ${token.symbol}`; // eg 1.23 DAI
return [
`Token: ${token.symbol} (${token.address})`,
`Best route for ${fromStr} to USDC: ${JSON.stringify(route, null, 2)}`,
].join("\n");
}

async function grantInvite(kwargs: Map<string, string>): Promise<string> {
const code = Array(8)
.fill(0)
Expand Down Expand Up @@ -172,12 +282,16 @@ async function setMaxUses(kwargs: Map<string, string>) {
return `Successfully updated invite: ${res}\n\n${getJSONblock(inviteStatus)}`;
}

async function help(extraText?: string) {
async function help(extraText?: string, cmdName?: string) {
let res = "";
if (extraText) res = `${extraText}\n\n`;
res += "Available commands:\n";
for (const [name, cmd] of Object.entries(commands)) {
res += `${name} ${cmd.help}\n`;
if (cmdName) {
res += `${cmdName}: ${commands[cmdName].help}\n`;
} else {
res += "Available commands:\n";
for (const [name, cmd] of Object.entries(commands)) {
res += `${name} ${cmd.help}\n`;
}
}
return res;
}
17 changes: 17 additions & 0 deletions apps/daimo-clippy/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { App } from "@slack/bolt";

import { handleCommand } from "./handlers";

const readline = require("readline/promises");

//
// Slack App, using Bolt. Subscribes to events (eg, someone used a slash
// command), handles commands.
Expand All @@ -25,4 +27,19 @@ app.event("app_mention", async ({ event, say }) => {
await app.start(port);

console.log(`⚡️ Slackbot is running on port ${port}`);

// For faster development, test locally:
console.log(`To test @Clippy locally, enter commands below:`);
const { stdin: input, stdout: output } = process;
const rl = readline.createInterface({ input, output });
while (true) {
const input = (await rl.question("@Clippy ")).trim();
if (input === "") continue;
try {
const response = await handleCommand(`<@U0610TSAFAR> ${input}`);
console.log(response);
} catch (e) {
console.log(e);
}
}
})();
1 change: 1 addition & 0 deletions apps/daimo-clippy/src/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { chainConfig } from "./env";

const apiUrl = process.env.DAIMO_API_URL || "http://localhost:3000";
export const apiUrlWithChain = `${apiUrl}/chain/${chainConfig.chainL2.id}`;
console.log(`[CLIPPY] using API URL ${apiUrlWithChain}`);

export const rpc = createTRPCClient<AppRouter>({
links: [httpBatchLink({ url: apiUrlWithChain })],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ function DepositAddressBottomSheetInner({ account }: { account: Account }) {
/>
<Spacer h={16} />
<TextPara color={color.grayDark}>
Send {tokenSymbol} to your address below. You can also send any ERC-20
token, and it'll be converted to USDC. Confirm that you're sending:
Send {tokenSymbol} to your address below. Any other ERC-20 tokens will
be converted to USDC. Confirm that you're sending:
</TextPara>
<Spacer h={12} />
<CheckLabel value={check} setValue={setCheck}>
Expand Down
2 changes: 1 addition & 1 deletion packages/daimo-api/src/contract/ethIndexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,6 @@ export class ETHIndexer extends Indexer {

console.log(`[ETH] getProposedSwap ${addr}: ${JSON.stringify(swap)}`);

return swap ? [swap] : [];
return swap && swap.routeFound ? [swap] : [];
}
}
27 changes: 25 additions & 2 deletions packages/daimo-api/src/contract/foreignCoinIndexer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
BigIntStr,
ProposedSwap,
guessTimestampFromNum,
isAmountDust,
Expand Down Expand Up @@ -41,7 +42,7 @@ export class ForeignCoinIndexer extends Indexer {

private listeners: ((transfers: ForeignTokenTransfer[]) => void)[] = [];

constructor(private nameReg: NameRegistry, private uc: UniswapClient) {
constructor(private nameReg: NameRegistry, public uc: UniswapClient) {
super("SWAPCOIN");
}

Expand Down Expand Up @@ -188,7 +189,9 @@ export class ForeignCoinIndexer extends Indexer {
`[SWAPCOIN] getProposedSwapForLog ${log.from}: ${JSON.stringify(swap)}`
);

if (swap && isAmountDust(swap.toAmount, log.foreignToken)) return null;
if (!swap) return null;
if (!swap.routeFound) return null;
if (isAmountDust(swap.toAmount, log.foreignToken)) return null;
return swap;
}

Expand Down Expand Up @@ -228,4 +231,24 @@ export class ForeignCoinIndexer extends Indexer {
foreignToken: this.foreignTokens.get(log.foreignToken.token)!,
};
}

// For debugging / introspection of Uniswap routes
public async getProposedSwap(
fromAmount: BigIntStr,
fromToken: Address,
toAddr: Address
) {
const coin = this.foreignTokens.get(fromToken);
if (coin == null) return null;
return this.uc.getProposedSwap(
toAddr,
fromAmount,
coin,
0,
{
addr: getAddress("0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead"),
},
true
);
}
}
Loading

0 comments on commit 817e1da

Please sign in to comment.