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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Token" ADD COLUMN "assetIssuer" TEXT;
4 changes: 4 additions & 0 deletions apps/backend-relayer/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ model Token {
kind TokenKind @default(ERC20)
decimals Int

/// Stellar classic-asset issuer (G-strkey). Populated only for SAC tokens;
/// null for NATIVE/ERC20/SEP41. Asset code is read from `symbol`.
assetIssuer String?

chainUid String
chain Chain @relation(fields: [chainUid], references: [id], onDelete: Cascade)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,11 @@ export abstract class ChainAdapter {

abstract orderTypeHash(orderParams: T_OrderParams): string;

// Signature is a `0x`-hex blob on EVM (EIP-712) and a base64 (or `0x`-hex)
// SEP-43-style signature on Stellar. Narrow per-adapter as needed.
abstract verifyOrderSignature(
address: ChainAddress,
orderHash: `0x${string}`,
signature: `0x${string}`,
signature: string,
): boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {
@Injectable()
export class EvmChainAdapter extends ChainAdapter {
private static readonly EVM_ADDRESS_RE = /^0x[a-fA-F0-9]{40}$/;
// ECDSA signature: r(32) + s(32) + v(1) = 65 bytes → 130 hex chars.
private static readonly EVM_SIG_RE = /^0x[a-fA-F0-9]{130}$/;

constructor(private readonly viem: ViemService) {
super();
Expand All @@ -41,6 +43,14 @@ export class EvmChainAdapter extends ChainAdapter {
}
}

private assertLocalSignature(value: string, field: string): void {
if (!EvmChainAdapter.EVM_SIG_RE.test(value)) {
throw new Error(
`${field}: expected ECDSA signature (0x + 130 hex), got "${value}"`,
);
}
}

getCreateAdRequestContractDetails(
data: T_CreateAdRequest,
): Promise<T_CreateAdRequestContractDetails> {
Expand Down Expand Up @@ -183,13 +193,14 @@ export class EvmChainAdapter extends ChainAdapter {
verifyOrderSignature(
address: ChainAddress,
orderHash: `0x${string}`,
signature: `0x${string}`,
signature: string,
): boolean {
this.assertLocalAddress(address, 'address');
this.assertLocalSignature(signature, 'signature');
return this.viem.verifyOrderSignature(
address as `0x${string}`,
orderHash,
signature,
signature as `0x${string}`,
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export class StellarChainAdapter extends ChainAdapter {
verifyOrderSignature(
address: ChainAddress,
orderHash: `0x${string}`,
signature: `0x${string}`,
signature: string,
): boolean {
this.assertLocalAddress(address, 'address');
return this.stellar.verifyOrderSignature(
Expand Down
10 changes: 9 additions & 1 deletion apps/backend-relayer/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,15 @@ async function bootstrap() {

app.use(morgan('tiny'));
app.use(express.json({ limit: 5 << 20 }));
app.useGlobalPipes(new ValidationPipe({ transform: true }));
// whitelist drops unknown fields; forbidNonWhitelisted turns them into 400s
// so clients get loud feedback on typos instead of silent data loss.
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
}),
);
app.useGlobalFilters(new GlobalExceptionFilter());

const swaggerOptions = new DocumentBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ describe('AdminController (unit)', () => {
chain: {} as any,
};

const created = { id: 'tok-1', ...dto };
const created = { id: 'tok-1', assetIssuer: null, ...dto };

const spy = jest
.spyOn(service, 'createToken')
Expand Down
40 changes: 32 additions & 8 deletions apps/backend-relayer/src/modules/ads/ad.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,22 +116,24 @@ export class AdsService {
maxAmount: true,
adToken: {
select: {
chain: { select: { chainId: true } },
chain: { select: { chainId: true, kind: true } },
address: true,
symbol: true,
name: true,
decimals: true,
kind: true,
assetIssuer: true,
},
},
orderToken: {
select: {
chain: { select: { chainId: true } },
chain: { select: { chainId: true, kind: true } },
address: true,
symbol: true,
name: true,
decimals: true,
kind: true,
assetIssuer: true,
},
},
status: true,
Expand Down Expand Up @@ -197,15 +199,19 @@ export class AdsService {
address: i.adToken.address,
decimals: i.adToken.decimals,
chainId: i.adToken.chain.chainId.toString(),
chainKind: i.adToken.chain.kind as string,
kind: i.adToken.kind as string,
assetIssuer: i.adToken.assetIssuer,
},
orderToken: {
name: i.orderToken.name,
symbol: i.orderToken.symbol,
address: i.orderToken.address,
decimals: i.orderToken.decimals,
chainId: i.orderToken.chain.chainId.toString(),
chainKind: i.orderToken.chain.kind as string,
kind: i.orderToken.kind as string,
assetIssuer: i.orderToken.assetIssuer,
},
};
});
Expand Down Expand Up @@ -244,22 +250,24 @@ export class AdsService {
metadata: true,
adToken: {
select: {
chain: { select: { chainId: true } },
chain: { select: { chainId: true, kind: true } },
address: true,
symbol: true,
name: true,
decimals: true,
kind: true,
assetIssuer: true,
},
},
orderToken: {
select: {
chain: { select: { chainId: true } },
chain: { select: { chainId: true, kind: true } },
address: true,
symbol: true,
name: true,
decimals: true,
kind: true,
assetIssuer: true,
},
},
createdAt: true,
Expand Down Expand Up @@ -301,15 +309,19 @@ export class AdsService {
address: ad.adToken.address,
decimals: ad.adToken.decimals,
chainId: ad.adToken.chain.chainId.toString(),
chainKind: ad.adToken.chain.kind as string,
kind: ad.adToken.kind as string,
assetIssuer: ad.adToken.assetIssuer,
},
orderToken: {
name: ad.orderToken.name,
symbol: ad.orderToken.symbol,
address: ad.orderToken.address,
decimals: ad.orderToken.decimals,
chainId: ad.orderToken.chain.chainId.toString(),
chainKind: ad.orderToken.chain.kind as string,
kind: ad.orderToken.kind as string,
assetIssuer: ad.orderToken.assetIssuer,
},
metadata: ad.metadata ?? null,
createdAt: ad.createdAt.toISOString(),
Expand Down Expand Up @@ -497,7 +509,10 @@ export class AdsService {
return reqContractDetails;
});

return requestDetails;
return {
...requestDetails,
chainKind: route.adToken.chain.kind as string,
};
} catch (e) {
if (e instanceof Error) {
if (e instanceof HttpException) throw e;
Expand Down Expand Up @@ -618,7 +633,10 @@ export class AdsService {
return entry;
});

return reqContractDetails;
return {
...reqContractDetails,
chainKind: ad.route.adToken.chain.kind as string,
};
} catch (e) {
if (e instanceof Error) {
if (e instanceof HttpException) throw e;
Expand Down Expand Up @@ -762,7 +780,10 @@ export class AdsService {
}
});

return reqContractDetails;
return {
...reqContractDetails,
chainKind: ad.route.adToken.chain.kind as string,
};
} catch (e) {
if (e instanceof Error) {
if (e instanceof HttpException) throw e;
Expand Down Expand Up @@ -975,7 +996,10 @@ export class AdsService {
}
});

return reqContractDetails;
return {
...reqContractDetails,
chainKind: ad.route.adToken.chain.kind as string,
};
} catch (e) {
if (e instanceof Error) {
if (e instanceof HttpException) throw e;
Expand Down
37 changes: 34 additions & 3 deletions apps/backend-relayer/src/modules/ads/dto/ad.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,13 +241,20 @@ export class TokenDto {
symbol!: string;
@ApiProperty({ type: String, description: 'Token contract address' })
address!: string;
@ApiProperty({ type: String, description: 'Token decimal places' })
@ApiProperty({ type: Number, description: 'Token decimal places' })
decimals!: number;
@ApiProperty({ type: String, description: 'Blockchain chain ID' })
chainId!: string;
@ApiProperty({
enum: ['ERC20', 'NATIVE'],
description: 'Token kind (e.g., ERC20, NATIVE)',
enum: ['EVM', 'STELLAR'],
description:
'Kind of the underlying chain — tells the client which address format and signing scheme the token lives on.',
})
chainKind!: string;
@ApiProperty({
enum: ['ERC20', 'NATIVE', 'SAC', 'SEP41'],
description:
'Token kind (EVM: ERC20/NATIVE; Stellar: NATIVE/SAC/SEP41)',
})
kind!: string;
}
Expand Down Expand Up @@ -410,6 +417,12 @@ export class CreateAdResponseDto {
'0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
})
reqHash!: `0x${string}`;

@ApiProperty({
enum: ['EVM', 'STELLAR'],
description: 'Kind of chain the ad contract runs on',
})
chainKind!: 'EVM' | 'STELLAR';
}
export class FundAdResponseDto {
@ApiProperty({
Expand Down Expand Up @@ -461,6 +474,12 @@ export class FundAdResponseDto {
'0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
})
reqHash!: `0x${string}`;

@ApiProperty({
enum: ['EVM', 'STELLAR'],
description: 'Kind of chain the ad contract runs on',
})
chainKind!: 'EVM' | 'STELLAR';
}

export class WithdrawAdResponseDto {
Expand Down Expand Up @@ -519,6 +538,12 @@ export class WithdrawAdResponseDto {
'0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
})
reqHash!: `0x${string}`;

@ApiProperty({
enum: ['EVM', 'STELLAR'],
description: 'Kind of chain the ad contract runs on',
})
chainKind!: 'EVM' | 'STELLAR';
}

export class ConfirmChainActionADResponseDto {
Expand Down Expand Up @@ -621,4 +646,10 @@ export class CloseAdResponseDto {
'0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef',
})
reqHash!: `0x${string}`;

@ApiProperty({
enum: ['EVM', 'STELLAR'],
description: 'Kind of chain the ad contract runs on',
})
chainKind!: 'EVM' | 'STELLAR';
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ describe('ChainController (unit)', () => {
afterEach(() => jest.resetAllMocks());

it('list -> delegates to service', async () => {
const mock = { rows: [], nextCursor: null };
const mock = { data: [], nextCursor: null };
const spy = jest
.spyOn(service, 'listChainsPublic')
.mockResolvedValueOnce(mock);
Expand Down
6 changes: 3 additions & 3 deletions apps/backend-relayer/src/modules/chains/chain.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ export class ChainService {

async listChainsPublic(query: QueryChainsDto) {
try {
const data = await this.listChains(query);
const rows = data.rows.map((c) => this.toPublic(c));
return { rows, nextCursor: data.nextCursor };
const listed = await this.listChains(query);
const data = listed.rows.map((c) => this.toPublic(c));
return { data, nextCursor: listed.nextCursor };
} catch (e) {
if (e instanceof Error) {
const status = e.message.toLowerCase().includes('forbidden')
Expand Down
2 changes: 1 addition & 1 deletion apps/backend-relayer/src/modules/chains/dto/chain.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ export class ChainResponseDto {

export class ListChainsResponseDto {
@ApiProperty({ type: [ChainResponseDto] })
rows!: ChainResponseDto[];
data!: ChainResponseDto[];

@ApiProperty({
description: 'Next pagination cursor',
Expand Down
Loading
Loading