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
45 changes: 44 additions & 1 deletion src/components/dashboard/Webhooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
WebhookEndpoint,
WebhookEvent,
WebhookEventType,
WebhookProvider,
} from '../../lib/webhooks';

export const Webhooks: React.FC = () => {
Expand Down Expand Up @@ -234,6 +235,19 @@ const EndpointCard: React.FC<{
<code style={{ fontSize: '0.85rem', color: 'var(--text-primary)' }}>{endpoint.url}</code>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.375rem' }}>
{endpoint.provider && endpoint.provider !== 'custom' && (
<span
style={{
padding: '0.25rem 0.5rem',
background: 'var(--primary-light)',
borderRadius: '0.25rem',
fontSize: '0.75rem',
color: 'var(--primary)',
}}
>
{endpoint.provider === 'zapier' ? 'Zapier' : 'Make.com'}
</span>
)}
{endpoint.events.map((event) => (
<span
key={event}
Expand Down Expand Up @@ -382,6 +396,7 @@ const CreateEndpointModal: React.FC<{
onSuccess: () => void;
}> = ({ onClose, onSuccess }) => {
const [url, setUrl] = useState('');
const [provider, setProvider] = useState<WebhookProvider>('custom');
const [selectedEvents, setSelectedEvents] = useState<WebhookEventType[]>(['all']);
const [submitting, setSubmitting] = useState(false);

Expand All @@ -393,7 +408,12 @@ const CreateEndpointModal: React.FC<{

setSubmitting(true);
try {
await webhookManager.createEndpoint(url, selectedEvents);
await webhookManager.createEndpoint(
url,
selectedEvents,
provider === 'custom' ? undefined : { integration: provider },
provider,
);
onSuccess();
} catch (error) {
alert('Failed to create endpoint: ' + (error instanceof Error ? error.message : 'Unknown error'));
Expand Down Expand Up @@ -467,6 +487,29 @@ const CreateEndpointModal: React.FC<{
/>
</div>

<div style={{ marginBottom: '1.5rem' }}>
<label style={{ display: 'block', marginBottom: '0.5rem', fontSize: '0.9rem', fontWeight: 500 }}>
Automation Platform
</label>
<select
value={provider}
onChange={(e) => setProvider(e.target.value as WebhookProvider)}
style={{
width: '100%',
padding: '0.75rem',
border: '1px solid var(--border)',
borderRadius: '0.375rem',
background: 'var(--surface-1)',
color: 'var(--text-primary)',
fontSize: '0.9rem',
}}
>
<option value="custom">Custom webhook</option>
<option value="zapier">Zapier</option>
<option value="make">Make.com</option>
</select>
</div>

<div style={{ marginBottom: '1.5rem' }}>
<label style={{ display: 'block', marginBottom: '0.5rem', fontSize: '0.9rem', fontWeight: 500 }}>
Event Types
Expand Down
90 changes: 90 additions & 0 deletions src/lib/__tests__/automationIntegrations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { describe, expect, it } from 'vitest'
import {
createPaymentActionDraft,
createTransactionTriggerPayload,
makeAppDefinition,
mapTransactionToWebhookEventType,
zapierAppDefinition,
} from '../automationIntegrations'

describe('automationIntegrations', () => {
it('defines Zapier transaction trigger and payment action', () => {
expect(zapierAppDefinition.triggers.newTransaction.operation.type).toBe('hook')
expect(zapierAppDefinition.triggers.newTransaction.operation.event).toBe(
'transaction.created',
)
expect(zapierAppDefinition.creates.createPaymentDraft.operation.perform).toBe(
'createAutomationPaymentDraft',
)
})

it('defines Make.com trigger and action modules', () => {
expect(makeAppDefinition.modules.watchTransactions.type).toBe('trigger')
expect(makeAppDefinition.modules.watchTransactions.webhook).toBe(true)
expect(makeAppDefinition.modules.createPaymentDraft.type).toBe('action')
})

it('maps transaction notifications to automation trigger payloads', () => {
const timestamp = Date.UTC(2026, 0, 2, 3, 4, 5)
const payload = createTransactionTriggerPayload({
id: 'notif-1',
accountId: 'GACCOUNT',
transaction: { hash: 'tx-hash' },
timestamp,
type: 'payment',
amount: '10',
asset: 'XLM',
from: 'GFROM',
to: 'GTO',
status: 'success',
network: 'testnet',
})

expect(mapTransactionToWebhookEventType('payment')).toBe('payment')
expect(payload).toMatchObject({
event: 'transaction.created',
hash: 'tx-hash',
amount: '10',
asset: 'XLM',
network: 'testnet',
occurredAt: '2026-01-02T03:04:05.000Z',
})
})

it('creates payment action drafts for automation platforms', () => {
const draft = createPaymentActionDraft(
{
sourceAccount: ' GSOURCE ',
destination: ' GDEST ',
amount: ' 12.5 ',
memo: ' invoice-42 ',
},
'zapier',
)

expect(draft).toMatchObject({
type: 'payment',
status: 'draft',
provider: 'zapier',
params: {
sourceAccount: 'GSOURCE',
memo: 'invoice-42',
baseFee: 100,
network: 'testnet',
},
})
expect(draft.params.operations).toEqual([
{ type: 'payment', destination: 'GDEST', amount: '12.5' },
])
})

it('rejects invalid payment action amounts', () => {
expect(() =>
createPaymentActionDraft({
sourceAccount: 'GSOURCE',
destination: 'GDEST',
amount: '0',
}),
).toThrow('Payment amount must be greater than zero')
})
})
211 changes: 211 additions & 0 deletions src/lib/automationIntegrations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { buildTransaction, type BuildTransactionParams, type NetworkName } from './stellar'
import { webhookManager, type WebhookEventType } from './webhooks'

export type AutomationProvider = 'zapier' | 'make'

export interface AutomationTransaction {
id: string
accountId: string
transaction: Record<string, unknown>
timestamp: number
type: 'payment' | 'trade' | 'contract' | 'other'
amount?: string
asset?: string
from?: string
to?: string
status: 'success' | 'pending' | 'failed'
network: 'mainnet' | 'testnet'
}

export interface PaymentAutomationInput {
sourceAccount: string
destination: string
amount: string
memo?: string
network?: NetworkName
baseFee?: number
}

export interface PaymentAutomationDraft {
type: 'payment'
status: 'draft'
provider?: AutomationProvider
params: BuildTransactionParams
}

export const automationEventTypes: WebhookEventType[] = [
'payment',
'trust',
'contract',
'account_merge',
]

export const zapierAppDefinition = {
key: 'stellarDevDashboard',
name: 'Stellar Dev Dashboard',
version: '1.0.0',
authentication: {
type: 'custom',
fields: [{ key: 'webhookUrl', label: 'Dashboard webhook URL', required: true }],
},
triggers: {
newTransaction: {
key: 'new_transaction',
noun: 'Transaction',
display: {
label: 'New Stellar Transaction',
description: 'Triggers when a monitored Stellar account receives a transaction.',
},
operation: {
type: 'hook',
event: 'transaction.created',
performSubscribe: 'createZapierTransactionHook',
performUnsubscribe: 'deleteAutomationHook',
performList: 'listRecentAutomationEvents',
},
},
},
creates: {
createPaymentDraft: {
key: 'create_payment_draft',
noun: 'Payment Draft',
display: {
label: 'Create Stellar Payment',
description: 'Builds an unsigned Stellar payment transaction for signing and submission.',
},
operation: {
perform: 'createAutomationPaymentDraft',
},
},
},
} as const

export const makeAppDefinition = {
name: 'stellar-dev-dashboard',
label: 'Stellar Dev Dashboard',
version: '1.0.0',
modules: {
watchTransactions: {
type: 'trigger',
label: 'Watch Transactions',
webhook: true,
event: 'transaction.created',
output: ['id', 'accountId', 'hash', 'type', 'amount', 'asset', 'from', 'to', 'network'],
},
createPaymentDraft: {
type: 'action',
label: 'Create Payment Draft',
input: ['sourceAccount', 'destination', 'amount', 'memo', 'network'],
output: ['type', 'status', 'params'],
},
},
} as const

export function mapTransactionToWebhookEventType(
transactionType: AutomationTransaction['type'],
): WebhookEventType {
if (transactionType === 'payment' || transactionType === 'trade') return 'payment'
if (transactionType === 'contract') return 'contract'
return 'all'
}

export function createTransactionTriggerPayload(transaction: AutomationTransaction) {
const hash =
typeof transaction.transaction.hash === 'string'
? transaction.transaction.hash
: transaction.id

return {
id: transaction.id,
event: 'transaction.created',
accountId: transaction.accountId,
hash,
type: transaction.type,
amount: transaction.amount,
asset: transaction.asset,
from: transaction.from,
to: transaction.to,
status: transaction.status,
network: transaction.network,
occurredAt: new Date(transaction.timestamp).toISOString(),
transaction: transaction.transaction,
}
}

export async function createAutomationEndpoint(
provider: AutomationProvider,
url: string,
events: WebhookEventType[] = ['payment', 'contract'],
) {
return webhookManager.createEndpoint(
url,
events,
{
provider,
integration: provider === 'zapier' ? 'Zapier' : 'Make.com',
},
provider,
)
}

export async function triggerTransactionAutomation(
transaction: AutomationTransaction,
): Promise<void> {
await webhookManager.triggerEvent(
mapTransactionToWebhookEventType(transaction.type),
createTransactionTriggerPayload(transaction),
)
}

export function createPaymentActionDraft(
input: PaymentAutomationInput,
provider?: AutomationProvider,
): PaymentAutomationDraft {
const amount = input.amount.trim()

if (!input.sourceAccount.trim()) {
throw new Error('Source account is required')
}

if (!input.destination.trim()) {
throw new Error('Destination account is required')
}

if (!amount || Number(amount) <= 0) {
throw new Error('Payment amount must be greater than zero')
}

return {
type: 'payment',
status: 'draft',
provider,
params: {
sourceAccount: input.sourceAccount.trim(),
operations: [
{
type: 'payment',
destination: input.destination.trim(),
amount,
},
],
memo: input.memo?.trim() || undefined,
baseFee: input.baseFee ?? 100,
timeBounds: {},
network: input.network ?? 'testnet',
},
}
}

export async function executePaymentAutomationAction(
input: PaymentAutomationInput,
provider?: AutomationProvider,
) {
const draft = createPaymentActionDraft(input, provider)
const transaction = await buildTransaction(draft.params)

return {
...draft,
status: 'draft' as const,
xdr: transaction.toXDR(),
}
}
Loading
Loading