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: add receipts endpoint #275

Merged
merged 3 commits into from
Nov 13, 2023
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: 0 additions & 1 deletion filecoin/store/receipt.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ export const useReceiptStore = (s3client, invocationBucketName, workflowBucketNa
headers: {},
})


// @ts-expect-error unknown link does not mach expectations
const receipt = agentMessage.receipts.get(invocationCid.toString())
if (!receipt) {
Expand Down
22 changes: 21 additions & 1 deletion stacks/ucan-invocation-stack.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
Queue,
use
} from '@serverless-stack/resources'
import { PolicyStatement, StarPrincipal, Effect } from 'aws-cdk-lib/aws-iam'

import { CarparkStack } from './carpark-stack.js'
import { UploadDbStack } from './upload-db-stack.js'
Expand Down Expand Up @@ -33,9 +34,28 @@ export function UcanInvocationStack({ stack, app }) {
const workflowBucket = new Bucket(stack, 'workflow-store', {
cors: true,
cdk: {
bucket: getBucketConfig('workflow-store', app.stage)
bucket: {
...getBucketConfig('workflow-store', app.stage),
// change the defaults accordingly to allow access via new Policy
blockPublicAccess: {
blockPublicAcls: true,
ignorePublicAcls: true,
restrictPublicBuckets: false,
blockPublicPolicy: false,
}
},
}
})
// Make bucket public for `s3:GetObject` command
workflowBucket.cdk.bucket.addToResourcePolicy(
new PolicyStatement({
actions: ['s3:GetObject'],
effect: Effect.ALLOW,
principals: [new StarPrincipal()],
resources: [workflowBucket.cdk.bucket.arnForObjects('*')],
})
)

const invocationBucket = new Bucket(stack, 'invocation-store', {
cors: true,
cdk: {
Expand Down
1 change: 1 addition & 0 deletions stacks/upload-api-stack.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ export function UploadApiStack({ stack, app }) {
'GET /error': 'functions/get.error',
'GET /version': 'functions/get.version',
'GET /metrics': 'functions/metrics.handler',
'GET /receipt/{taskCid}': 'functions/receipt.handler',
'GET /storefront-cron': 'functions/storefront-cron.handler',
// AWS API Gateway does not know trailing slash... and Grafana Agent puts trailing slash
'GET /metrics/{proxy+}': 'functions/metrics.handler',
Expand Down
30 changes: 30 additions & 0 deletions test/filecoin.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { testFilecoin as test } from './helpers/context.js'
import { fetch } from '@web-std/fetch'
import pWaitFor from 'p-wait-for'
import * as CAR from '@ucanto/transport/car'
import { Storefront } from '@web3-storage/filecoin-client'
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'

Expand Down Expand Up @@ -119,6 +120,35 @@ test('w3filecoin integration flow', async t => {
throw new Error('filecoin/offer receipt has no effect for filecoin/accept')
}

// Get receipt from endpoint
const filecoinOfferInvCid = filecoinOfferRes.ran.link()
const workflowWithReceiptResponse = await fetch(
`${t.context.apiEndpoint}/receipt/${filecoinOfferInvCid.toString()}`,
{
redirect: 'manual'
}
)
t.is(workflowWithReceiptResponse.status, 302)
const workflowLocation = workflowWithReceiptResponse.headers.get('location')
if (!workflowLocation) {
throw new Error(`no workflow with receipt for task cid ${filecoinOfferInvCid.toString()}`)
}

const workflowWithReceiptResponseAfterRedirect = await fetch(workflowLocation)
// Get receipt from Message Archive
const agentMessageBytes = new Uint8Array((await workflowWithReceiptResponseAfterRedirect.arrayBuffer()))
const agentMessage = await CAR.request.decode({
body: agentMessageBytes,
headers: {},
})
// @ts-expect-error unknown link does not mach expectations
const receipt = agentMessage.receipts.get(filecoinOfferInvCid.toString())
t.assert(receipt)
// Receipt matches what we received when invoked
t.truthy(receipt?.ran.link().equals(filecoinOfferInvCid))
t.truthy(receipt?.fx.join?.equals(filecoinAcceptReceiptCid))
t.truthy(receipt?.fx.fork[0].equals(filecoinSubmitReceiptCid))

// Verify receipt chain
console.log(`wait for receipt chain...`)
const receiptStore = useReceiptStore(s3Client, `invocation-store-${stage}-0`, `workflow-store-${stage}-0`)
Expand Down
4 changes: 2 additions & 2 deletions upload-api/buckets/invocation-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,12 @@ export const useInvocationStore = (s3client, bucketName) => {
})
const listObject = await s3client.send(listObjectCmd)
const carEntry = listObject.Contents?.find(
content => content.Key?.endsWith('.workflow')
content => content.Key?.endsWith('.out')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😱 this had a bug, but this code path was not being used. See https://github.com/web3-storage/w3infra/blob/main/docs/ucan-invocation-stream.md#buckets as this first was .workflow but then was iterated to .in and .out

)
if (!carEntry) {
return
}
return carEntry.Key?.replace(prefix, '').replace('.workflow', '')
return carEntry.Key?.replace(prefix, '').replace('.out', '')
}
}
}
57 changes: 57 additions & 0 deletions upload-api/functions/receipt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import * as Sentry from '@sentry/serverless'
import { parseLink } from '@ucanto/server'

import { createInvocationStore } from '../buckets/invocation-store.js'
import { mustGetEnv } from './utils.js'

Sentry.AWSLambda.init({
environment: process.env.SST_STAGE,
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 1.0,
})

const AWS_REGION = process.env.AWS_REGION || 'us-west-2'

/**
* AWS HTTP Gateway handler for GET /receipt.
*
* @param {import('aws-lambda').APIGatewayProxyEventV2} event
*/
export async function receiptGet (event) {
const {
invocationBucketName,
workflowBucketName,
} = getLambdaEnv()
const invocationBucket = createInvocationStore(
AWS_REGION,
invocationBucketName
)

if (!event.pathParameters?.taskCid) {
return {
statusCode: 400,
body: Buffer.from(`no task cid received`).toString('base64'),
}
}
const taskCid = parseLink(event.pathParameters.taskCid)

const workflowLinkWithReceipt = await invocationBucket.getWorkflowLink(taskCid.toString())
const url = `https://${workflowBucketName}.s3.${AWS_REGION}.amazonaws.com/${workflowLinkWithReceipt}/${workflowLinkWithReceipt}`

// redirect to bucket
return {
statusCode: 302,
headers: {
Location: url
}
}
}

function getLambdaEnv () {
return {
invocationBucketName: mustGetEnv('INVOCATION_BUCKET_NAME'),
workflowBucketName: mustGetEnv('WORKFLOW_BUCKET_NAME'),
}
}

export const handler = Sentry.AWSLambda.wrapHandler(receiptGet)
3 changes: 3 additions & 0 deletions upload-api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { CID } from 'multiformats/cid'
import { Kinesis } from '@aws-sdk/client-kinesis'
import { AccountDID, ProviderDID, Service, SpaceDID } from '@web3-storage/upload-api'

export interface StoreOperationError extends Error {
name: 'StoreOperationFailed'
}

export interface UcanLogCtx extends WorkflowCtx, ReceiptBlockCtx {
basicAuth: string
Expand Down
Loading