mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 03:19:46 +00:00
feat: implement GET statuses/:id{[0-9a-f]{64}}/nutzapped_by (with tests)
This commit is contained in:
parent
71a558a9de
commit
8a75f9e944
2 changed files with 176 additions and 1 deletions
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Proof } from '@cashu/cashu-ts';
|
||||||
import { proofSchema, walletSchema } from '@ditto/cashu';
|
import { proofSchema, walletSchema } from '@ditto/cashu';
|
||||||
import { DittoConf } from '@ditto/conf';
|
import { DittoConf } from '@ditto/conf';
|
||||||
import { type User } from '@ditto/mastoapi/middleware';
|
import { type User } from '@ditto/mastoapi/middleware';
|
||||||
|
|
@ -10,9 +11,9 @@ import { assertArrayIncludes, assertEquals, assertExists, assertObjectMatch } fr
|
||||||
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
||||||
|
|
||||||
import cashuRoute from '@/controllers/api/cashu.ts';
|
import cashuRoute from '@/controllers/api/cashu.ts';
|
||||||
|
import { accountFromPubkey } from '@/views/mastodon/accounts.ts';
|
||||||
import { createTestDB } from '@/test.ts';
|
import { createTestDB } from '@/test.ts';
|
||||||
import { nostrNow } from '@/utils.ts';
|
import { nostrNow } from '@/utils.ts';
|
||||||
import { Proof } from '@cashu/cashu-ts';
|
|
||||||
|
|
||||||
Deno.test('PUT /wallet must be successful', async () => {
|
Deno.test('PUT /wallet must be successful', async () => {
|
||||||
const mock = stub(globalThis, 'fetch', () => {
|
const mock = stub(globalThis, 'fetch', () => {
|
||||||
|
|
@ -1158,6 +1159,136 @@ Deno.test('POST /nutzap must be successful WITHOUT proofs to keep', async () =>
|
||||||
mock.restore();
|
mock.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test('GET /statuses/:id{[0-9a-f]{64}}/nutzapped_by must be successful', async () => {
|
||||||
|
const mock = stub(globalThis, 'fetch', () => {
|
||||||
|
return Promise.resolve(new Response());
|
||||||
|
});
|
||||||
|
|
||||||
|
await using test = await createTestRoute();
|
||||||
|
const { route, sk, relay, signer } = test;
|
||||||
|
|
||||||
|
const pubkey = await signer.getPublicKey();
|
||||||
|
|
||||||
|
const post = genEvent({
|
||||||
|
kind: 1,
|
||||||
|
content: 'Hello',
|
||||||
|
}, sk);
|
||||||
|
await relay.event(post);
|
||||||
|
|
||||||
|
const senderSk = generateSecretKey();
|
||||||
|
const sender = getPublicKey(senderSk);
|
||||||
|
|
||||||
|
await relay.event(genEvent({
|
||||||
|
created_at: nostrNow() - 1,
|
||||||
|
kind: 9321,
|
||||||
|
content: 'Who do I have?',
|
||||||
|
tags: [
|
||||||
|
['e', post.id],
|
||||||
|
['p', pubkey],
|
||||||
|
['u', 'https://mint.soul.com'],
|
||||||
|
[
|
||||||
|
'proof',
|
||||||
|
'{"amount":1,"C":"02277c66191736eb72fce9d975d08e3191f8f96afb73ab1eec37e4465683066d3f","id":"000a93d6f8a1d2c4","secret":"[\\"P2PK\\",{\\"nonce\\":\\"b00bdd0467b0090a25bdf2d2f0d45ac4e355c482c1418350f273a04fedaaee83\\",\\"data\\":\\"02eaee8939e3565e48cc62967e2fde9d8e2a4b3ec0081f29eceff5c64ef10ac1ed\\"}]"}',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'proof',
|
||||||
|
'{"amount":1,"C":"02277c66191736eb72fce9d975d08e3191f8f96afb73ab1eec37e4465683066d3f","id":"000a93d6f8a1d2c4","secret":"[\\"P2PK\\",{\\"nonce\\":\\"b00bdd0467b0090a25bdf2d2f0d45ac4e355c482c1418350f273a04fedaaee83\\",\\"data\\":\\"02eaee8939e3565e48cc62967e2fde9d8e2a4b3ec0081f29eceff5c64ef10ac1ed\\"}]"}',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}, senderSk));
|
||||||
|
|
||||||
|
await relay.event(genEvent({
|
||||||
|
created_at: nostrNow() - 3,
|
||||||
|
kind: 9321,
|
||||||
|
content: 'Want it all to end',
|
||||||
|
tags: [
|
||||||
|
['e', post.id],
|
||||||
|
['p', pubkey],
|
||||||
|
['u', 'https://mint.soul.com'],
|
||||||
|
[
|
||||||
|
'proof',
|
||||||
|
JSON.stringify({
|
||||||
|
id: '005c2502034d4f12',
|
||||||
|
amount: 25,
|
||||||
|
secret: 'z+zyxAVLRqN9lEjxuNPSyRJzEstbl69Jc1vtimvtkPg=',
|
||||||
|
C: '0241d98a8197ef238a192d47edf191a9de78b657308937b4f7dd0aa53beae72c46',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}, senderSk));
|
||||||
|
|
||||||
|
await relay.event(genEvent({
|
||||||
|
created_at: nostrNow() - 5,
|
||||||
|
kind: 9321,
|
||||||
|
content: 'Evidence',
|
||||||
|
tags: [
|
||||||
|
['e', post.id],
|
||||||
|
['p', pubkey],
|
||||||
|
['u', 'https://mint.soul.com'],
|
||||||
|
[
|
||||||
|
'proof',
|
||||||
|
'{"amount":1,"C":"02277c66191736eb72fce9d975d08e3191f8f96afb73ab1eec37e4465683066d3f","id":"000a93d6f8a1d2c4","secret":"[\\"P2PK\\",{\\"nonce\\":\\"b00bdd0467b0090a25bdf2d2f0d45ac4e355c482c1418350f273a04fedaaee83\\",\\"data\\":\\"02eaee8939e3565e48cc62967e2fde9d8e2a4b3ec0081f29eceff5c64ef10ac1ed\\"}]"}',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}, senderSk));
|
||||||
|
|
||||||
|
const sender2Sk = generateSecretKey();
|
||||||
|
const sender2 = getPublicKey(sender2Sk);
|
||||||
|
|
||||||
|
await relay.event(genEvent({
|
||||||
|
created_at: nostrNow() + 10,
|
||||||
|
kind: 9321,
|
||||||
|
content: 'Reach out',
|
||||||
|
tags: [
|
||||||
|
['e', post.id],
|
||||||
|
['p', pubkey],
|
||||||
|
['u', 'https://mint.soul.com'],
|
||||||
|
[
|
||||||
|
'proof',
|
||||||
|
JSON.stringify({
|
||||||
|
id: '005c2502034d4f12',
|
||||||
|
amount: 25,
|
||||||
|
secret: 'z+zyxAVLRqN9lEjxuNPSyRJzEstbl69Jc1vtimvtkPg=',
|
||||||
|
C: '0241d98a8197ef238a192d47edf191a9de78b657308937b4f7dd0aa53beae72c46',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}, sender2Sk));
|
||||||
|
|
||||||
|
const response = await route.request(`/statuses/${post.id}/nutzapped_by`, {
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
|
||||||
|
const body = await response.json();
|
||||||
|
|
||||||
|
assertEquals(response.status, 200);
|
||||||
|
|
||||||
|
assertEquals(body, [
|
||||||
|
{
|
||||||
|
comment: 'Reach out',
|
||||||
|
amount: 25,
|
||||||
|
account: JSON.parse(JSON.stringify(accountFromPubkey(sender2))),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comment: 'Who do I have?',
|
||||||
|
amount: 2,
|
||||||
|
account: JSON.parse(JSON.stringify(accountFromPubkey(sender))),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comment: 'Want it all to end',
|
||||||
|
amount: 25,
|
||||||
|
account: JSON.parse(JSON.stringify(accountFromPubkey(sender))),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
comment: 'Evidence',
|
||||||
|
amount: 1,
|
||||||
|
account: JSON.parse(JSON.stringify(accountFromPubkey(sender))),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
mock.restore();
|
||||||
|
});
|
||||||
|
|
||||||
async function createTestRoute() {
|
async function createTestRoute() {
|
||||||
const conf = new DittoConf(
|
const conf = new DittoConf(
|
||||||
new Map([['DITTO_NSEC', 'nsec14fg8xd04hvlznnvhaz77ms0k9kxy9yegdsgs2ar27czhh46xemuquqlv0m']]),
|
new Map([['DITTO_NSEC', 'nsec14fg8xd04hvlznnvhaz77ms0k9kxy9yegdsgs2ar27czhh46xemuquqlv0m']]),
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { CashuMint, CashuWallet, MintQuoteState, Proof } from '@cashu/cashu-ts';
|
||||||
import {
|
import {
|
||||||
getWallet,
|
getWallet,
|
||||||
organizeProofs,
|
organizeProofs,
|
||||||
|
proofSchema,
|
||||||
renderTransaction,
|
renderTransaction,
|
||||||
tokenEventSchema,
|
tokenEventSchema,
|
||||||
validateAndParseWallet,
|
validateAndParseWallet,
|
||||||
|
|
@ -14,6 +15,7 @@ import { generateSecretKey, getPublicKey } from 'nostr-tools';
|
||||||
import { NostrEvent, NSchema as n } from '@nostrify/nostrify';
|
import { NostrEvent, NSchema as n } from '@nostrify/nostrify';
|
||||||
import { bytesToString, stringToBytes } from '@scure/base';
|
import { bytesToString, stringToBytes } from '@scure/base';
|
||||||
import { logi } from '@soapbox/logi';
|
import { logi } from '@soapbox/logi';
|
||||||
|
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { createEvent, parseBody } from '@/utils/api.ts';
|
import { createEvent, parseBody } from '@/utils/api.ts';
|
||||||
|
|
@ -294,6 +296,48 @@ route.get('/transactions', userMiddleware({ enc: 'nip44' }), async (c) => {
|
||||||
return paginated(c, events, transactions);
|
return paginated(c, events, transactions);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Gets the nutzaps that a post received. */
|
||||||
|
route.get('statuses/:id{[0-9a-f]{64}}/nutzapped_by', async (c) => {
|
||||||
|
const id = c.req.param('id');
|
||||||
|
const { relay, signal } = c.var;
|
||||||
|
const { limit, since, until } = paginationSchema().parse(c.req.query());
|
||||||
|
|
||||||
|
const events = await relay.query([{ kinds: [9321], '#e': [id], since, until, limit }], {
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!events.length) {
|
||||||
|
return c.json([], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
await hydrateEvents({ ...c.var, events });
|
||||||
|
|
||||||
|
const results = (await Promise.all(
|
||||||
|
events.map((event: DittoEvent) => {
|
||||||
|
const proofs = (event.tags.filter(([name]) => name === 'proof').map(([_, proof]) => {
|
||||||
|
const { success, data } = n.json().pipe(proofSchema).safeParse(proof);
|
||||||
|
if (!success) return;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.filter(Boolean)) as Proof[];
|
||||||
|
|
||||||
|
const amount = proofs.reduce((prev, current) => prev + current.amount, 0);
|
||||||
|
const comment = event.content;
|
||||||
|
|
||||||
|
const account = event?.author ? renderAccount(event.author) : accountFromPubkey(event.pubkey);
|
||||||
|
|
||||||
|
return {
|
||||||
|
comment,
|
||||||
|
amount,
|
||||||
|
account,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
)).filter(Boolean);
|
||||||
|
|
||||||
|
return paginated(c, events, results);
|
||||||
|
});
|
||||||
|
|
||||||
/** Get mints set by the CASHU_MINTS environment variable. */
|
/** Get mints set by the CASHU_MINTS environment variable. */
|
||||||
route.get('/mints', (c) => {
|
route.get('/mints', (c) => {
|
||||||
const { conf } = c.var;
|
const { conf } = c.var;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue