Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
c9fd607
Added invalid JSON check on payment triggers.
Klakurka Jul 28, 2025
bfa6073
Fixed triggerService tests.
Klakurka Jul 28, 2025
58f7ad6
Removed superfluous comment.
Klakurka Aug 9, 2025
0bb70f9
Output addresses post data feature starting point.
Klakurka Aug 17, 2025
92fb5b7
Commit fix.
Klakurka Aug 17, 2025
469b4c5
Consistent ordering of outputAddresses POST data variable.
Klakurka Aug 18, 2025
ebdc0e7
feat: add console.error
chedieck Aug 18, 2025
b2bf96f
Merge branch 'master' into fix/invalid-json-on-payment-triggers
chedieck Aug 18, 2025
ce1ea1f
test: fix imports
chedieck Aug 18, 2025
7366534
Updated coding instructions.
Klakurka Aug 18, 2025
342f5c5
Consolidated payment trigger tests, fixed commit issue.
Klakurka Aug 18, 2025
fd8d82b
Revert "Consolidated payment trigger tests, fixed commit issue."
Klakurka Aug 18, 2025
6641bca
Revert "Commit fix."
Klakurka Aug 18, 2025
cd317f9
Added a few payment trigger tests.
Klakurka Aug 18, 2025
5e9f33f
Fixed trigger tests.
Klakurka Aug 19, 2025
f93d516
fix: ignore undefined fields signature
lissavxo Aug 20, 2025
38da54b
Updated <inputAddresses> and <outputAddresses> post msg variables to …
Klakurka Aug 20, 2025
f952128
Updated tests.
Klakurka Aug 20, 2025
2789f31
Typo.
Klakurka Aug 20, 2025
4c33fb0
Merge pull request #1028 from PayButton/fix/invalid-json-on-payment-t…
chedieck Aug 25, 2025
11af13c
Merge pull request #1048 from PayButton/fix/payload_signature
chedieck Aug 25, 2025
09d24ec
Revert "Fixed trigger tests."
Klakurka Aug 28, 2025
fce8330
Cleaned up some redant decimal handling.
Klakurka Aug 28, 2025
e788611
chore(lint-staged): use node to invoke eslint for cross-platform hook
Klakurka Sep 9, 2025
80a641c
Output addresses post data feature starting point.
Klakurka Aug 17, 2025
fee3ef6
Commit fix.
Klakurka Aug 17, 2025
a881f3c
Consistent ordering of outputAddresses POST data variable.
Klakurka Aug 18, 2025
db715a3
Revert "Commit fix."
Klakurka Aug 18, 2025
2b474d9
Added a few payment trigger tests.
Klakurka Aug 18, 2025
2d5ee0c
Fixed trigger tests.
Klakurka Aug 19, 2025
dea696b
Updated <inputAddresses> and <outputAddresses> post msg variables to …
Klakurka Aug 20, 2025
3ee8486
Updated tests.
Klakurka Aug 20, 2025
1b090fa
Revert "Fixed trigger tests."
Klakurka Aug 28, 2025
2a653f9
Cleaned up some redant decimal handling.
Klakurka Aug 28, 2025
d22b02e
Merge remote-tracking branch 'origin/feat/output-addresses-in-post-da…
Klakurka Sep 9, 2025
0f9705e
Moved const out of loop.
Klakurka Sep 9, 2025
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
2 changes: 1 addition & 1 deletion .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
#!/bin/sh
npx lint-staged
yarn lint-staged
1 change: 1 addition & 0 deletions .github/coding-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ When writing tests, follow these guidelines to ensure consistency and maintainab
- **Make tests resilient to refactoring**: Tests should pass even if internal implementation changes, as long as the behavior remains the same.
- Never make up new functions just to make tests pass. Always build tests based on the functions that already exist. If a function needs to be updated/revised/refactored, that is also OK.
- Do not just add a 'markTestSkipped' on tests that look difficult to write. Instead, explain the problem and ask for some additional context before trying again.
- Make sure new tests added into the "integration-tests" directory are actually integration tests.

General guidlines:
- Never edit files that are git ignored.
Expand Down
1 change: 1 addition & 0 deletions components/Paybutton/PaybuttonTrigger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ export default ({ paybuttonId, emailCredits }: IProps): JSX.Element => {
<div>&lt;opReturn&gt;</div>
<div>&lt;signature&gt;</div>
<div>&lt;inputAddresses&gt;</div>
<div>&lt;outputAddresses&gt;</div>
<div>&lt;value&gt;</div>

</div>
Expand Down
1 change: 1 addition & 0 deletions constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,7 @@ export const TRIGGER_POST_VARIABLES = [
'<timestamp>',
'<txId>',
'<inputAddresses>',
'<outputAddresses>',
'<value>'
]

Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"ci:integration:test": "yarn pretest && dotenv -e .env.test -- ts-node -O '{\"module\":\"commonjs\"}' node_modules/jest/bin/jest.js tests/integration-tests --forceExit",
"tarDebug": "tar cf debug.tar logs/ paybutton-config.json .env*",
"updateAllPrices": "./scripts/update-all-prices.sh",
"updateAllPriceConnections": "./scripts/update-all-price-connections.sh"
"updateAllPriceConnections": "./scripts/update-all-price-connections.sh",
"lint-staged": "lint-staged"
},
"dependencies": {
"@emotion/react": "^11.8.2",
Expand Down Expand Up @@ -99,8 +100,8 @@
"chronik-client-cashtokens/ecashaddrjs": "^2.0.0"
},
"lint-staged": {
"*.ts?(x)": [
"yarn eslint --fix"
"*.{ts,tsx}": [
"node ./node_modules/eslint/bin/eslint.js --fix"
]
}
}
45 changes: 36 additions & 9 deletions services/chronikService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,22 +387,47 @@ export class ChronikBlockchainClient {
}
}

private getSortedInputAddresses (transaction: Tx): string[] {
private getSortedInputAddresses (transaction: Tx): Array<{address: string, amount: Prisma.Decimal}> {
const addressSatsMap = new Map<string, bigint>()

transaction.inputs.forEach((inp) => {
const address = outputScriptToAddress(this.networkSlug, inp.outputScript)
if (address !== undefined && address !== '') {
const currentValue = addressSatsMap.get(address) ?? 0n
addressSatsMap.set(address, currentValue + inp.sats)
}
})

const unitDivisor = this.networkId === XEC_NETWORK_ID
? 1e2
: (this.networkId === BCH_NETWORK_ID ? 1e8 : 1)
const sortedInputAddresses = Array.from(addressSatsMap.entries())
.sort(([, valueA], [, valueB]) => Number(valueB - valueA))
.map(([address]) => address)
return sortedInputAddresses.map(([address, sats]) => {
const decimal = new Prisma.Decimal(sats.toString())
const amount = decimal.dividedBy(unitDivisor)
return { address, amount }
})
}

return sortedInputAddresses
private getSortedOutputAddresses (transaction: Tx): Array<{address: string, amount: Prisma.Decimal}> {
const addressSatsMap = new Map<string, bigint>()
transaction.outputs.forEach((out) => {
const address = outputScriptToAddress(this.networkSlug, out.outputScript)
if (address !== undefined && address !== '') {
const currentValue = addressSatsMap.get(address) ?? 0n
addressSatsMap.set(address, currentValue + out.sats)
}
})
const unitDivisor = this.networkId === XEC_NETWORK_ID
? 1e2
: (this.networkId === BCH_NETWORK_ID ? 1e8 : 1)
const sortedOutputAddresses = Array.from(addressSatsMap.entries())
.sort(([, valueA], [, valueB]) => Number(valueB - valueA))
.map(([address, sats]) => {
const decimal = new Prisma.Decimal(sats.toString())
const amount = decimal.dividedBy(unitDivisor)
return { address, amount }
})
return sortedOutputAddresses
}

public async waitForSyncing (txId: string, addressStringArray: string[]): Promise<void> {
Expand Down Expand Up @@ -449,10 +474,11 @@ export class ChronikBlockchainClient {
const addressesWithTransactions = await this.getAddressesForTransaction(transaction)
await this.waitForSyncing(msg.txid, addressesWithTransactions.map(obj => obj.address.address))
const inputAddresses = this.getSortedInputAddresses(transaction)
const outputAddresses = this.getSortedOutputAddresses(transaction)
for (const addressWithTransaction of addressesWithTransactions) {
const { created, tx } = await upsertTransaction(addressWithTransaction.transaction)
if (tx !== undefined) {
const broadcastTxData = this.broadcastIncomingTx(addressWithTransaction.address.address, tx, inputAddresses)
const broadcastTxData = this.broadcastIncomingTx(addressWithTransaction.address.address, tx, inputAddresses, outputAddresses)
if (created) { // only execute trigger for newly added txs
await executeAddressTriggers(broadcastTxData, tx.address.networkId)
}
Expand All @@ -476,11 +502,11 @@ export class ChronikBlockchainClient {
}
}

private broadcastIncomingTx (addressString: string, createdTx: TransactionWithAddressAndPrices, inputAddresses: string[]): BroadcastTxData {
private broadcastIncomingTx (addressString: string, createdTx: TransactionWithAddressAndPrices, inputAddresses: Array<{address: string, amount: Prisma.Decimal}>, outputAddresses: Array<{address: string, amount: Prisma.Decimal}>): BroadcastTxData {
const broadcastTxData: BroadcastTxData = {} as BroadcastTxData
broadcastTxData.address = addressString
broadcastTxData.messageType = 'NewTx'
const newSimplifiedTransaction = getSimplifiedTrasaction(createdTx, inputAddresses)
const newSimplifiedTransaction = getSimplifiedTrasaction(createdTx, inputAddresses, outputAddresses)
broadcastTxData.txs = [newSimplifiedTransaction]
try { // emit broadcast for both unconfirmed and confirmed txs
this.wsEndpoint.emit(SOCKET_MESSAGES.TXS_BROADCAST, broadcastTxData)
Expand All @@ -504,11 +530,12 @@ export class ChronikBlockchainClient {
for (const transaction of blockTxsToSync) {
const addressesWithTransactions = await this.getAddressesForTransaction(transaction)
const inputAddresses = this.getSortedInputAddresses(transaction)
const outputAddresses = this.getSortedOutputAddresses(transaction)

for (const addressWithTransaction of addressesWithTransactions) {
const { created, tx } = await upsertTransaction(addressWithTransaction.transaction)
if (tx !== undefined) {
const broadcastTxData = this.broadcastIncomingTx(addressWithTransaction.address.address, tx, inputAddresses)
const broadcastTxData = this.broadcastIncomingTx(addressWithTransaction.address.address, tx, inputAddresses, outputAddresses)
if (created) { // only execute trigger for newly added txs
await executeAddressTriggers(broadcastTxData, tx.address.networkId)
}
Expand Down
3 changes: 2 additions & 1 deletion services/transactionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function getSimplifiedTransactions (transactionsToPersist: TransactionWit
return simplifiedTransactions
}

export function getSimplifiedTrasaction (tx: TransactionWithAddressAndPrices, inputAddresses?: string[]): SimplifiedTransaction {
export function getSimplifiedTrasaction (tx: TransactionWithAddressAndPrices, inputAddresses?: Array<{address: string, amount: Prisma.Decimal}>, outputAddresses?: Array<{address: string, amount: Prisma.Decimal}>): SimplifiedTransaction {
const {
hash,
amount,
Expand All @@ -63,6 +63,7 @@ export function getSimplifiedTrasaction (tx: TransactionWithAddressAndPrices, in
message: parsedOpReturn?.message ?? '',
rawMessage: parsedOpReturn?.rawMessage ?? '',
inputAddresses: inputAddresses ?? [],
outputAddresses: outputAddresses ?? [],
prices: tx.prices
}

Expand Down
42 changes: 39 additions & 3 deletions services/triggerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,8 @@ export async function executeAddressTriggers (broadcastTxData: BroadcastTxData,
paymentId,
message,
rawMessage,
inputAddresses
inputAddresses,
outputAddresses
} = tx
const values = getTransactionValue(tx)
const addressTriggers = await fetchTriggersForAddress(address)
Expand All @@ -258,6 +259,14 @@ export async function executeAddressTriggers (broadcastTxData: BroadcastTxData,
await Promise.all(posterTriggers.map(async (trigger) => {
const userProfile = await fetchUserFromTriggerId(trigger.id)
const quoteSlug = SUPPORTED_QUOTES_FROM_ID[userProfile.preferredCurrencyId]
// We ensure that the primary address (<address> variable) is the first element in the outputAddresses since this is likely more useful for apps using the data than it would be if it was in a random order.
let reorderedOutputAddresses = outputAddresses
if (Array.isArray(outputAddresses)) {
const primary = reorderedOutputAddresses.find(oa => oa.address === address)
if (primary !== undefined) {
reorderedOutputAddresses = [primary, ...reorderedOutputAddresses.filter(o => o.address !== address)]
}
}
const postDataParameters: PostDataParameters = {
amount,
currency,
Expand All @@ -273,6 +282,7 @@ export async function executeAddressTriggers (broadcastTxData: BroadcastTxData,
}
: EMPTY_OP_RETURN,
inputAddresses,
outputAddresses: reorderedOutputAddresses,
value: values[quoteSlug].toString()
}

Expand Down Expand Up @@ -395,20 +405,46 @@ export interface PostDataParameters {
buttonName: string
address: string
opReturn: OpReturnData
inputAddresses?: string[]
inputAddresses?: Array<{address: string, amount: Prisma.Decimal}>
outputAddresses?: Array<{address: string, amount: Prisma.Decimal}>
value: string
}

async function postDataForTrigger (trigger: TriggerWithPaybutton, postDataParameters: PostDataParameters): Promise<void> {
const actionType: TriggerLogActionType = 'PostData'
let logData!: PostDataTriggerLog | PostDataTriggerLogError
let isError = false

// Validate JSON first before attempting network request
let parsedPostDataParameters: any
try {
const parsedPostDataParameters = parseTriggerPostData({
parsedPostDataParameters = parseTriggerPostData({
userId: trigger.paybutton.providerUserId,
postData: trigger.postData,
postDataParameters
})
} catch (jsonErr: any) {
isError = true
logData = {
errorName: jsonErr.name ?? 'JSON_VALIDATION_ERROR',
errorMessage: jsonErr.message ?? 'Invalid JSON in trigger post data',
errorStack: jsonErr.stack ?? '',
triggerPostData: trigger.postData,
triggerPostURL: trigger.postURL
}
await prisma.triggerLog.create({
data: {
triggerId: trigger.id,
isError,
actionType,
data: JSON.stringify(logData)
}
})
console.error(`[ERROR] Invalid trigger data in DB for trigger ${trigger.id} (should never happen)`)
return
}

try {
const response = await axios.post(
trigger.postURL,
parsedPostDataParameters,
Expand Down
25 changes: 25 additions & 0 deletions tests/unittests/transactionService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,28 @@ describe('Fetch transactions by paybuttonId', () => {
}
})
})

describe('Address object arrays (input/output) integration', () => {
it('getSimplifiedTrasaction returns provided input/output address objects untouched', () => {
const tx: any = {
hash: 'hash1',
amount: new Prisma.Decimal(5),
confirmed: true,
opReturn: '',
address: { address: 'ecash:qqprimaryaddressxxxxxxxxxxxxxxxxxxxxx' },
timestamp: 1700000000,
prices: mockedTransaction.prices
}
const inputs = [
{ address: 'ecash:qqinput1', amount: new Prisma.Decimal(1.23) },
{ address: 'ecash:qqinput2', amount: new Prisma.Decimal(4.56) }
]
const outputs = [
{ address: 'ecash:qqout1', amount: new Prisma.Decimal(7.89) },
{ address: 'ecash:qqout2', amount: new Prisma.Decimal(0.12) }
]
const simplified = transactionService.getSimplifiedTrasaction(tx, inputs, outputs)
expect(simplified.inputAddresses).toEqual(inputs)
expect(simplified.outputAddresses).toEqual(outputs)
})
})
Loading