Skip to content
Open
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
58 changes: 58 additions & 0 deletions .github/workflows/build-migration.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: "Build migration script image"

on:
workflow_call:
inputs:
push:
required: true
type: boolean
target:
required: true
type: string
base_image:
required: true
type: string

jobs:
build:
name: Build
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
# Checkout repository under $GITHUB_WORKSPACE path
- name: Repository checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PUSH_TOKEN }}

# Configure tag name
- name: Sets env vars
run: |
echo "DOCKER_IMAGE_NAME=${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }}-migration:${GITHUB_SHA:0:8}" >> $GITHUB_ENV

# Build docker image
- name: Build docker image
uses: docker/build-push-action@v4
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Update docker/build-push-action to a newer version.

The workflow uses docker/build-push-action@v4, which is outdated and may not work on current GitHub Actions runners. Update to v6 for compatibility and latest features.

Apply this diff:

       - name: Build docker image
-        uses: docker/build-push-action@v4
+        uses: docker/build-push-action@v6
         with:

Based on learnings

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uses: docker/build-push-action@v4
- name: Build docker image
uses: docker/build-push-action@v6
with:
🧰 Tools
🪛 actionlint (1.7.8)

47-47: the runner of "docker/build-push-action@v4" action is too old to run on GitHub Actions. update the action's version to fix this issue

(action)

🤖 Prompt for AI Agents
.github/workflows/build-migration.yaml around line 47: the workflow pins
docker/build-push-action@v4 which is outdated; update the action reference to
docker/build-push-action@v6 (replace the v4 tag with v6) to use the newer
release and ensure compatibility with current GitHub Actions runners, then
commit and run the workflow to verify the build/push step succeeds.

with:
context: .
file: docker/migration.Dockerfile
target: ${{ inputs.target }}
push: ${{ inputs.push }}
tags: ${{ env.DOCKER_IMAGE_NAME }}
build-args: |
BASE_IMAGE=${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }}-base:${{ inputs.base_image }}
TURBO_TEAM=peersyst
secrets: |
turbo_token=${{ secrets.TURBO_TOKEN }}
9 changes: 9 additions & 0 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,12 @@ jobs:
target: integration
push: false
base_image: ${{ github.sha }}

build-migration-script:
needs: [build-base]
uses: ./.github/workflows/build-migration.yaml
secrets: inherit
with:
target: integration
push: true
base_image: ${{ github.sha }}
2 changes: 1 addition & 1 deletion apps/relayer/openapi-spec.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"openapi":"3.0.0","paths":{"/api/metrics":{"get":{"operationId":"index","parameters":[],"responses":{"200":{"description":""}}}},"/api/relayer/fast-auth":{"post":{"operationId":"createAccount","summary":"Create account","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAccountRequest"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignResponse"}}}}},"tags":["fast-auth-relayer"]}}},"info":{"title":"relayer","description":"Relayer","version":"0.0.0","contact":{}},"tags":[],"servers":[],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"JWT","type":"http"},"X-API-KEY":{"type":"apiKey","in":"header","name":"X-API-KEY"}},"schemas":{"SignRequest":{"type":"object","properties":{}},"SignResponse":{"type":"object","properties":{}},"SignAndSendDelegateActionRequest":{"type":"object","properties":{}},"CreateAccountRequest":{"type":"object","properties":{}}}}}
{"openapi":"3.0.0","paths":{"/api/metrics":{"get":{"operationId":"index","parameters":[],"responses":{"200":{"description":""}}}},"/api/relayer/fast-auth":{"post":{"operationId":"createAccount","summary":"Create account","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAccountRequest"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignResponse"}}}}},"tags":["fast-auth-relayer"]}},"/api/relayer/fast-auth/create_account_atomic":{"post":{"operationId":"createAccountAtomic","summary":"Create account","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAccountAtomicRequest"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignResponse"}}}}},"tags":["fast-auth-relayer"]}}},"info":{"title":"relayer","description":"Relayer","version":"0.0.0","contact":{}},"tags":[],"servers":[],"components":{"securitySchemes":{"bearer":{"scheme":"bearer","bearerFormat":"JWT","type":"http"},"X-API-KEY":{"type":"apiKey","in":"header","name":"X-API-KEY"}},"schemas":{"SignRequest":{"type":"object","properties":{}},"SignResponse":{"type":"object","properties":{}},"SignAndSendDelegateActionRequest":{"type":"object","properties":{}},"CreateAccountRequest":{"type":"object","properties":{}},"CreateAccountAtomicRequest":{"type":"object","properties":{}}}}}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Complete the CreateAccountAtomicRequest schema definition.

The schema for CreateAccountAtomicRequest has empty properties, which means the OpenAPI spec doesn't document the expected request structure. Based on the implementation in apps/relayer/src/modules/relayer/requests/create-account-atomic.request.ts, the schema should include: account_id, allowance, oauth_token, and signed_delegate_action.

This prevents API consumers from understanding the required fields and their types. Please regenerate the OpenAPI spec to include the complete schema definition.

🧰 Tools
🪛 Checkov (3.2.334)

[high] 1: Ensure that the global security field has rules defined

(CKV_OPENAPI_4)


[high] 1: Ensure that security operations is not empty.

(CKV_OPENAPI_5)

🤖 Prompt for AI Agents
In apps/relayer/openapi-spec.json around lines 1 to 1, the
CreateAccountAtomicRequest schema is empty; update the
components.schemas.CreateAccountAtomicRequest entry to include the expected
fields from
apps/relayer/src/modules/relayer/requests/create-account-atomic.request.ts
(account_id, allowance, oauth_token, signed_delegate_action) and regenerate the
OpenAPI spec — for example define account_id as string, allowance as string (or
numeric if implementation expects number), oauth_token as string, and
signed_delegate_action as an object (or reference the existing delegate action
schema) so consumers can see required fields and their types.

8 changes: 8 additions & 0 deletions apps/relayer/src/modules/relayer/relayer.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ApiOperation } from "@nestjs/swagger";
import { SignResponse } from "./response/sign.response";
import { SignAndSendDelegateActionRequest } from "./requests/sign-and-send-delegate-action.request";
import { CreateAccountRequest } from "./requests/create-account.request";
import {CreateAccountAtomicRequest} from "./requests/create-account-atomic.request";

@ApiTags("fast-auth-relayer")
@Controller("relayer/fast-auth")
Expand Down Expand Up @@ -34,4 +35,11 @@ export class RelayerController {
async createAccount(@Body() body: CreateAccountRequest): Promise<SignResponse> {
return this.fastAuthRelayerService.createAccount(body);
}

@Post("/create_account_atomic")
@ApiOperation({ summary: "Create account" })
@ApiBody({ type: CreateAccountAtomicRequest })
async createAccountAtomic(@Body() body: CreateAccountAtomicRequest): Promise<SignResponse> {
return this.fastAuthRelayerService.createAccountAtomic(body);
}
Comment on lines +39 to +44
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add input validation for the new endpoint.

The createAccountAtomic method doesn't validate the input body, unlike other endpoints in this controller (e.g., line 20 calls validate(body)). This could allow invalid or malicious data to reach the service layer.

Apply this diff to add validation:

 @Post("/create_account_atomic")
 @ApiOperation({ summary: "Create account" })
 @ApiBody({ type: CreateAccountAtomicRequest })
 async createAccountAtomic(@Body() body: CreateAccountAtomicRequest): Promise<SignResponse> {
+    validate(body);
     return this.fastAuthRelayerService.createAccountAtomic(body);
 }

Also ensure that the CreateAccountAtomicRequest class includes appropriate validation decorators (e.g., from class-validator).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Post("/create_account_atomic")
@ApiOperation({ summary: "Create account" })
@ApiBody({ type: CreateAccountAtomicRequest })
async createAccountAtomic(@Body() body: CreateAccountAtomicRequest): Promise<SignResponse> {
return this.fastAuthRelayerService.createAccountAtomic(body);
}
@Post("/create_account_atomic")
@ApiOperation({ summary: "Create account" })
@ApiBody({ type: CreateAccountAtomicRequest })
async createAccountAtomic(@Body() body: CreateAccountAtomicRequest): Promise<SignResponse> {
validate(body);
return this.fastAuthRelayerService.createAccountAtomic(body);
}
🤖 Prompt for AI Agents
In apps/relayer/src/modules/relayer/relayer.controller.ts around lines 39 to 44,
the new createAccountAtomic endpoint is missing input validation; update the
controller method to call validate(body) (or await validate(body)) before
passing to fastAuthRelayerService.createAccountAtomic, and return/throw the
resulting validation errors consistently with the other endpoints; additionally
ensure the CreateAccountAtomicRequest DTO is annotated with appropriate
class-validator decorators (e.g., IsString/IsEmail/IsOptional/ValidateNested
etc.) and any nested types use Type decorators so validation runs correctly.

}
35 changes: 32 additions & 3 deletions apps/relayer/src/modules/relayer/relayer.service.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { Inject, Injectable, Logger } from "@nestjs/common";
import { SignRequest } from "./requests/sign.request";
import { createTransaction, functionCall, SCHEMA, Action, Signature } from "near-api-js/lib/transaction";
import { actionCreators, DelegateAction } from "@near-js/transactions";
import {actionCreators, buildDelegateAction, DelegateAction} from "@near-js/transactions";
import { parseNearAmount } from "near-api-js/lib/utils/format";
import { base_decode } from "near-api-js/lib/utils/serialize";
import {base_decode} from "near-api-js/lib/utils/serialize";
import { NearSignerService } from "../near/near-signer.service";
import { NearClientService } from "../near/near-client.service";
import { ConfigService } from "@nestjs/config";
import { FastAuthRelayerMetricsProvider } from "./relayer.metrics";
import { serialize } from "near-api-js/lib/utils";
import {PublicKey, serialize} from "near-api-js/lib/utils";
import { SignResponse } from "./response/sign.response";
import { SignAndSendDelegateActionRequest } from "./requests/sign-and-send-delegate-action.request";
import { CreateAccountRequest } from "./requests/create-account.request";
import {CreateAccountAtomicRequest} from "./requests/create-account-atomic.request";

@Injectable()
export class RelayerService {
Expand Down Expand Up @@ -105,6 +106,34 @@ export class RelayerService {
]);
}

async createAccountAtomic(body: CreateAccountAtomicRequest): Promise<SignResponse> {
const functionCallAction = body.signed_delegate_action.delegate_action.actions[0].FunctionCall;
const [sigAlgo, sigData] = body.signed_delegate_action.signature.split(":")
const signedDelegate = actionCreators.signedDelegate({
delegateAction: buildDelegateAction({
senderId: body.signed_delegate_action.delegate_action.sender_id,
receiverId: body.signed_delegate_action.delegate_action.receiver_id,
actions: [
functionCall(
functionCallAction.method_name,
JSON.parse(Buffer.from(functionCallAction.args, "base64").toString()),
functionCallAction.gas,
functionCallAction.deposit,
)
],
nonce: body.signed_delegate_action.delegate_action.nonce,
maxBlockHeight: body.signed_delegate_action.delegate_action.max_block_height,
publicKey: PublicKey.from(body.signed_delegate_action.delegate_action.public_key),
}),
signature: new Signature({
keyType: sigAlgo === "ed25519" ? 0 : 1,
data: base_decode(sigData),
// 245, 37, 132, 230, ... 63, 170, 6
}),
});
return await this.signAndSendTransaction(body.signed_delegate_action.delegate_action.sender_id, [signedDelegate]);
}
Comment on lines +109 to +135
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Add input validation for createAccountAtomic.

The method accesses nested properties and performs operations without validating the input structure. Malformed requests will cause cryptic runtime errors or security issues.

Add validation before processing:

 async createAccountAtomic(body: CreateAccountAtomicRequest): Promise<SignResponse> {
+    // Validate input structure
+    if (!body.signed_delegate_action?.delegate_action?.actions?.[0]?.FunctionCall) {
+        throw new BadRequestException('Missing or invalid delegate_action.actions[0].FunctionCall');
+    }
+    if (!body.signed_delegate_action.signature || typeof body.signed_delegate_action.signature !== 'string') {
+        throw new BadRequestException('Missing or invalid signature');
+    }
+    if (!body.signed_delegate_action.delegate_action.sender_id || !body.signed_delegate_action.delegate_action.receiver_id) {
+        throw new BadRequestException('Missing sender_id or receiver_id');
+    }
+
     const functionCallAction = body.signed_delegate_action.delegate_action.actions[0].FunctionCall;
     const [sigAlgo, sigData] = body.signed_delegate_action.signature.split(":")
+    if (!sigAlgo || !sigData) {
+        throw new BadRequestException('Invalid signature format, expected "algorithm:data"');
+    }
+    if (sigAlgo !== "ed25519" && sigAlgo !== "secp256k1") {
+        throw new BadRequestException(`Unsupported signature algorithm: ${sigAlgo}`);
+    }
+
+    let parsedArgs;
+    try {
+        parsedArgs = JSON.parse(Buffer.from(functionCallAction.args, "base64").toString());
+    } catch (e) {
+        throw new BadRequestException('Invalid base64 or JSON in function call args');
+    }
+
     const signedDelegate = actionCreators.signedDelegate({
         delegateAction: buildDelegateAction({
             senderId: body.signed_delegate_action.delegate_action.sender_id,
             receiverId: body.signed_delegate_action.delegate_action.receiver_id,
             actions: [
                 functionCall(
                     functionCallAction.method_name,
-                    JSON.parse(Buffer.from(functionCallAction.args, "base64").toString()),
+                    parsedArgs,
                     functionCallAction.gas,
                     functionCallAction.deposit,
                 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async createAccountAtomic(body: CreateAccountAtomicRequest): Promise<SignResponse> {
const functionCallAction = body.signed_delegate_action.delegate_action.actions[0].FunctionCall;
const [sigAlgo, sigData] = body.signed_delegate_action.signature.split(":")
const signedDelegate = actionCreators.signedDelegate({
delegateAction: buildDelegateAction({
senderId: body.signed_delegate_action.delegate_action.sender_id,
receiverId: body.signed_delegate_action.delegate_action.receiver_id,
actions: [
functionCall(
functionCallAction.method_name,
JSON.parse(Buffer.from(functionCallAction.args, "base64").toString()),
functionCallAction.gas,
functionCallAction.deposit,
)
],
nonce: body.signed_delegate_action.delegate_action.nonce,
maxBlockHeight: body.signed_delegate_action.delegate_action.max_block_height,
publicKey: PublicKey.from(body.signed_delegate_action.delegate_action.public_key),
}),
signature: new Signature({
keyType: sigAlgo === "ed25519" ? 0 : 1,
data: base_decode(sigData),
// 245, 37, 132, 230, ... 63, 170, 6
}),
});
return await this.signAndSendTransaction(body.signed_delegate_action.delegate_action.sender_id, [signedDelegate]);
}
async createAccountAtomic(body: CreateAccountAtomicRequest): Promise<SignResponse> {
// Validate input structure
if (!body.signed_delegate_action?.delegate_action?.actions?.[0]?.FunctionCall) {
throw new BadRequestException('Missing or invalid delegate_action.actions[0].FunctionCall');
}
if (!body.signed_delegate_action.signature || typeof body.signed_delegate_action.signature !== 'string') {
throw new BadRequestException('Missing or invalid signature');
}
if (!body.signed_delegate_action.delegate_action.sender_id || !body.signed_delegate_action.delegate_action.receiver_id) {
throw new BadRequestException('Missing sender_id or receiver_id');
}
const functionCallAction = body.signed_delegate_action.delegate_action.actions[0].FunctionCall;
const [sigAlgo, sigData] = body.signed_delegate_action.signature.split(":")
if (!sigAlgo || !sigData) {
throw new BadRequestException('Invalid signature format, expected "algorithm:data"');
}
if (sigAlgo !== "ed25519" && sigAlgo !== "secp256k1") {
throw new BadRequestException(`Unsupported signature algorithm: ${sigAlgo}`);
}
let parsedArgs;
try {
parsedArgs = JSON.parse(Buffer.from(functionCallAction.args, "base64").toString());
} catch (e) {
throw new BadRequestException('Invalid base64 or JSON in function call args');
}
const signedDelegate = actionCreators.signedDelegate({
delegateAction: buildDelegateAction({
senderId: body.signed_delegate_action.delegate_action.sender_id,
receiverId: body.signed_delegate_action.delegate_action.receiver_id,
actions: [
functionCall(
functionCallAction.method_name,
parsedArgs,
functionCallAction.gas,
functionCallAction.deposit,
)
],
nonce: body.signed_delegate_action.delegate_action.nonce,
maxBlockHeight: body.signed_delegate_action.delegate_action.max_block_height,
publicKey: PublicKey.from(body.signed_delegate_action.delegate_action.public_key),
}),
signature: new Signature({
keyType: sigAlgo === "ed25519" ? 0 : 1,
data: base_decode(sigData),
// 245, 37, 132, 230, ... 63, 170, 6
}),
});
return await this.signAndSendTransaction(body.signed_delegate_action.delegate_action.sender_id, [signedDelegate]);
}


/**
* Sign and sends a transaction using a signer.
* @param receiverId The receiver id of the transaction.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class CreateAccountAtomicRequest {
account_id: string;
allowance: number;
oauth_token: string;
signed_delegate_action: any;
}
Comment on lines +1 to +6
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add validation decorators and improve type safety.

The request class lacks validation decorators, which are essential for request DTOs in NestJS applications. Additionally, signed_delegate_action is typed as any, reducing type safety.

+import { IsString, IsNumber, IsNotEmpty, ValidateNested } from 'class-validator';
+import { Type } from 'class-transformer';
+
+// Define a proper type for signed delegate action
+interface SignedDelegateAction {
+    delegateAction: any; // or more specific type
+    signature: string;
+}
+
 export class CreateAccountAtomicRequest {
+    @IsString()
+    @IsNotEmpty()
     account_id: string;
+    
+    @IsNumber()
     allowance: number;
+    
+    @IsString()
+    @IsNotEmpty()
     oauth_token: string;
-    signed_delegate_action: any;
+    
+    @ValidateNested()
+    @Type(() => Object)
+    signed_delegate_action: SignedDelegateAction;
 }

Committable suggestion skipped: line range outside the PR's diff.

17 changes: 17 additions & 0 deletions docker/migration.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# syntax=docker/dockerfile:1
ARG BASE_IMAGE=base
FROM ${BASE_IMAGE} as integration
ARG TURBO_TEAM=peersyst
ENV TURBO_TEAM=$TURBO_TEAM

# Include migration script
COPY scripts/migration /project/scripts/migration

# Install migration script dependencies
RUN pnpm install

# Lint migration script
RUN --mount=type=secret,id=turbo_token,env=TURBO_TOKEN \
npx turbo run lint --filter=@fast-auth/migration

WORKDIR /project/scripts/migration
Loading