Compare commits
2 Commits
5cfefc1ca8
...
266e9a892d
Author | SHA1 | Date | |
---|---|---|---|
|
266e9a892d | ||
|
cc003a9a3e |
@ -23,7 +23,7 @@ export async function getCost ({ subName, parentId, uploadIds, boost = 0, bio },
|
|||||||
|
|
||||||
// sub allows freebies (or is a bio or a comment), cost is less than baseCost, not anon,
|
// sub allows freebies (or is a bio or a comment), cost is less than baseCost, not anon,
|
||||||
// cost must be greater than user's balance, and user has not disabled freebies
|
// cost must be greater than user's balance, and user has not disabled freebies
|
||||||
const freebie = (parentId || bio || sub?.allowFreebies) && cost <= baseCost && !!me &&
|
const freebie = (parentId || bio) && cost <= baseCost && !!me &&
|
||||||
cost > me?.msats && !me?.disableFreebies
|
cost > me?.msats && !me?.disableFreebies
|
||||||
|
|
||||||
return freebie ? BigInt(0) : BigInt(cost)
|
return freebie ? BigInt(0) : BigInt(cost)
|
||||||
|
@ -23,19 +23,19 @@ import performPaidAction from '../paidAction'
|
|||||||
|
|
||||||
function commentsOrderByClause (me, models, sort) {
|
function commentsOrderByClause (me, models, sort) {
|
||||||
if (sort === 'recent') {
|
if (sort === 'recent') {
|
||||||
return 'ORDER BY "Item".created_at DESC, "Item".id DESC'
|
return 'ORDER BY ("Item"."deletedAt" IS NULL) DESC, ("Item".cost > 0 OR "Item"."weightedVotes" - "Item"."weightedDownVotes" > 0) DESC, "Item".created_at DESC, "Item".id DESC'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (me && sort === 'hot') {
|
if (me && sort === 'hot') {
|
||||||
return `ORDER BY COALESCE(
|
return `ORDER BY ("Item"."deletedAt" IS NULL) DESC, COALESCE(
|
||||||
personal_hot_score,
|
personal_hot_score,
|
||||||
${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3)) DESC NULLS LAST,
|
${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3)) DESC NULLS LAST,
|
||||||
"Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
|
"Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
|
||||||
} else {
|
} else {
|
||||||
if (sort === 'top') {
|
if (sort === 'top') {
|
||||||
return `ORDER BY ${orderByNumerator(models, 0)} DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
|
return `ORDER BY ("Item"."deletedAt" IS NULL) DESC, ${orderByNumerator(models, 0)} DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
|
||||||
} else {
|
} else {
|
||||||
return `ORDER BY ${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
|
return `ORDER BY ("Item"."deletedAt" IS NULL) DESC, ${orderByNumerator(models, 0)}/POWER(GREATEST(3, EXTRACT(EPOCH FROM (now_utc() - "Item".created_at))/3600), 1.3) DESC NULLS LAST, "Item".msats DESC, ("Item".cost > 0) DESC, "Item".id DESC`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1290,11 +1290,7 @@ export const updateItem = async (parent, { sub: subName, forward, ...item }, { m
|
|||||||
const differentSub = subName && old.subName !== subName
|
const differentSub = subName && old.subName !== subName
|
||||||
if (differentSub) {
|
if (differentSub) {
|
||||||
const sub = await models.sub.findUnique({ where: { name: subName } })
|
const sub = await models.sub.findUnique({ where: { name: subName } })
|
||||||
if (old.cost === 0) {
|
if (sub.baseCost > old.sub.baseCost) {
|
||||||
if (!sub.allowFreebies) {
|
|
||||||
throw new GraphQLError(`~${subName} does not allow freebies`, { extensions: { code: 'BAD_INPUT' } })
|
|
||||||
}
|
|
||||||
} else if (sub.baseCost > old.sub.baseCost) {
|
|
||||||
throw new GraphQLError('cannot change to a more expensive sub', { extensions: { code: 'BAD_INPUT' } })
|
throw new GraphQLError('cannot change to a more expensive sub', { extensions: { code: 'BAD_INPUT' } })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ export default gql`
|
|||||||
|
|
||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
upsertSub(oldName: String, name: String!, desc: String, baseCost: Int!,
|
upsertSub(oldName: String, name: String!, desc: String, baseCost: Int!,
|
||||||
postTypes: [String!]!, allowFreebies: Boolean!,
|
postTypes: [String!]!,
|
||||||
billingType: String!, billingAutoRenew: Boolean!,
|
billingType: String!, billingAutoRenew: Boolean!,
|
||||||
moderated: Boolean!, nsfw: Boolean!): SubPaidAction!
|
moderated: Boolean!, nsfw: Boolean!): SubPaidAction!
|
||||||
paySub(name: String!): SubPaidAction!
|
paySub(name: String!): SubPaidAction!
|
||||||
@ -24,7 +24,7 @@ export default gql`
|
|||||||
toggleSubSubscription(name: String!): Boolean!
|
toggleSubSubscription(name: String!): Boolean!
|
||||||
transferTerritory(subName: String!, userName: String!): Sub
|
transferTerritory(subName: String!, userName: String!): Sub
|
||||||
unarchiveTerritory(name: String!, desc: String, baseCost: Int!,
|
unarchiveTerritory(name: String!, desc: String, baseCost: Int!,
|
||||||
postTypes: [String!]!, allowFreebies: Boolean!,
|
postTypes: [String!]!,
|
||||||
billingType: String!, billingAutoRenew: Boolean!,
|
billingType: String!, billingAutoRenew: Boolean!,
|
||||||
moderated: Boolean!, nsfw: Boolean!): SubPaidAction!
|
moderated: Boolean!, nsfw: Boolean!): SubPaidAction!
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ import { SubmitButton } from './form'
|
|||||||
|
|
||||||
const FeeButtonContext = createContext()
|
const FeeButtonContext = createContext()
|
||||||
|
|
||||||
export function postCommentBaseLineItems ({ baseCost = 1, comment = false, allowFreebies = true, me }) {
|
export function postCommentBaseLineItems ({ baseCost = 1, comment = false, me }) {
|
||||||
const anonCharge = me
|
const anonCharge = me
|
||||||
? {}
|
? {}
|
||||||
: {
|
: {
|
||||||
@ -29,7 +29,7 @@ export function postCommentBaseLineItems ({ baseCost = 1, comment = false, allow
|
|||||||
term: baseCost,
|
term: baseCost,
|
||||||
label: `${comment ? 'comment' : 'post'} cost`,
|
label: `${comment ? 'comment' : 'post'} cost`,
|
||||||
modifier: (cost) => cost + baseCost,
|
modifier: (cost) => cost + baseCost,
|
||||||
allowFreebies
|
allowFreebies: comment
|
||||||
},
|
},
|
||||||
...anonCharge
|
...anonCharge
|
||||||
}
|
}
|
||||||
|
@ -149,7 +149,7 @@ export function PostForm ({ type, sub, children }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FeeButtonProvider
|
<FeeButtonProvider
|
||||||
baseLineItems={sub ? postCommentBaseLineItems({ baseCost: sub.baseCost, allowFreebies: sub.allowFreebies, me: !!me }) : undefined}
|
baseLineItems={sub ? postCommentBaseLineItems({ baseCost: sub.baseCost, me: !!me }) : undefined}
|
||||||
useRemoteLineItems={postCommentUseRemoteLineItems({ me: !!me })}
|
useRemoteLineItems={postCommentUseRemoteLineItems({ me: !!me })}
|
||||||
>
|
>
|
||||||
<FormType sub={sub}>{children}</FormType>
|
<FormType sub={sub}>{children}</FormType>
|
||||||
|
@ -91,7 +91,6 @@ export default function TerritoryForm ({ sub }) {
|
|||||||
desc: sub?.desc || '',
|
desc: sub?.desc || '',
|
||||||
baseCost: sub?.baseCost || 10,
|
baseCost: sub?.baseCost || 10,
|
||||||
postTypes: sub?.postTypes || POST_TYPES,
|
postTypes: sub?.postTypes || POST_TYPES,
|
||||||
allowFreebies: typeof sub?.allowFreebies === 'undefined' ? true : sub?.allowFreebies,
|
|
||||||
billingType: sub?.billingType || 'MONTHLY',
|
billingType: sub?.billingType || 'MONTHLY',
|
||||||
billingAutoRenew: sub?.billingAutoRenew || false,
|
billingAutoRenew: sub?.billingAutoRenew || false,
|
||||||
moderated: sub?.moderated || false,
|
moderated: sub?.moderated || false,
|
||||||
@ -133,15 +132,9 @@ export default function TerritoryForm ({ sub }) {
|
|||||||
label='post cost'
|
label='post cost'
|
||||||
name='baseCost'
|
name='baseCost'
|
||||||
type='number'
|
type='number'
|
||||||
groupClassName='mb-2'
|
|
||||||
required
|
required
|
||||||
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
|
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}
|
||||||
/>
|
/>
|
||||||
<Checkbox
|
|
||||||
label='allow free posts'
|
|
||||||
name='allowFreebies'
|
|
||||||
groupClassName='ms-1'
|
|
||||||
/>
|
|
||||||
<CheckboxGroup label='post types' name='postTypes'>
|
<CheckboxGroup label='post types' name='postTypes'>
|
||||||
<Row>
|
<Row>
|
||||||
<Col xs={4} sm='auto'>
|
<Col xs={4} sm='auto'>
|
||||||
|
@ -223,10 +223,10 @@ export const UPDATE_COMMENT = gql`
|
|||||||
export const UPSERT_SUB = gql`
|
export const UPSERT_SUB = gql`
|
||||||
${PAID_ACTION}
|
${PAID_ACTION}
|
||||||
mutation upsertSub($oldName: String, $name: String!, $desc: String, $baseCost: Int!,
|
mutation upsertSub($oldName: String, $name: String!, $desc: String, $baseCost: Int!,
|
||||||
$postTypes: [String!]!, $allowFreebies: Boolean!, $billingType: String!,
|
$postTypes: [String!]!, $billingType: String!,
|
||||||
$billingAutoRenew: Boolean!, $moderated: Boolean!, $nsfw: Boolean!) {
|
$billingAutoRenew: Boolean!, $moderated: Boolean!, $nsfw: Boolean!) {
|
||||||
upsertSub(oldName: $oldName, name: $name, desc: $desc, baseCost: $baseCost,
|
upsertSub(oldName: $oldName, name: $name, desc: $desc, baseCost: $baseCost,
|
||||||
postTypes: $postTypes, allowFreebies: $allowFreebies, billingType: $billingType,
|
postTypes: $postTypes, billingType: $billingType,
|
||||||
billingAutoRenew: $billingAutoRenew, moderated: $moderated, nsfw: $nsfw) {
|
billingAutoRenew: $billingAutoRenew, moderated: $moderated, nsfw: $nsfw) {
|
||||||
result {
|
result {
|
||||||
name
|
name
|
||||||
@ -238,10 +238,10 @@ export const UPSERT_SUB = gql`
|
|||||||
export const UNARCHIVE_TERRITORY = gql`
|
export const UNARCHIVE_TERRITORY = gql`
|
||||||
${PAID_ACTION}
|
${PAID_ACTION}
|
||||||
mutation unarchiveTerritory($name: String!, $desc: String, $baseCost: Int!,
|
mutation unarchiveTerritory($name: String!, $desc: String, $baseCost: Int!,
|
||||||
$postTypes: [String!]!, $allowFreebies: Boolean!, $billingType: String!,
|
$postTypes: [String!]!, $billingType: String!,
|
||||||
$billingAutoRenew: Boolean!, $moderated: Boolean!, $nsfw: Boolean!) {
|
$billingAutoRenew: Boolean!, $moderated: Boolean!, $nsfw: Boolean!) {
|
||||||
unarchiveTerritory(name: $name, desc: $desc, baseCost: $baseCost,
|
unarchiveTerritory(name: $name, desc: $desc, baseCost: $baseCost,
|
||||||
postTypes: $postTypes, allowFreebies: $allowFreebies, billingType: $billingType,
|
postTypes: $postTypes, billingType: $billingType,
|
||||||
billingAutoRenew: $billingAutoRenew, moderated: $moderated, nsfw: $nsfw) {
|
billingAutoRenew: $billingAutoRenew, moderated: $moderated, nsfw: $nsfw) {
|
||||||
result {
|
result {
|
||||||
name
|
name
|
||||||
|
@ -7,7 +7,6 @@ export const SUB_FIELDS = gql`
|
|||||||
name
|
name
|
||||||
createdAt
|
createdAt
|
||||||
postTypes
|
postTypes
|
||||||
allowFreebies
|
|
||||||
rankingType
|
rankingType
|
||||||
billingType
|
billingType
|
||||||
billingCost
|
billingCost
|
||||||
|
@ -137,6 +137,10 @@ export const WALLET = gql`
|
|||||||
... on WalletNwc {
|
... on WalletNwc {
|
||||||
nwcUrlRecv
|
nwcUrlRecv
|
||||||
}
|
}
|
||||||
|
... on WalletPhoenixd {
|
||||||
|
url
|
||||||
|
secondaryPassword
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -173,6 +177,10 @@ export const WALLET_BY_TYPE = gql`
|
|||||||
... on WalletNwc {
|
... on WalletNwc {
|
||||||
nwcUrlRecv
|
nwcUrlRecv
|
||||||
}
|
}
|
||||||
|
... on WalletPhoenixd {
|
||||||
|
url
|
||||||
|
secondaryPassword
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -762,6 +762,26 @@ export const lncSchema = object({
|
|||||||
.required('required')
|
.required('required')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const phoenixdSchema = object().shape({
|
||||||
|
url: string().url().required('required').trim(),
|
||||||
|
primaryPassword: string().length(64).hex()
|
||||||
|
.when(['secondaryPassword'], ([secondary], schema) => {
|
||||||
|
if (!secondary) return schema.required('required if secondary password not set')
|
||||||
|
return schema.test({
|
||||||
|
test: primary => secondary !== primary,
|
||||||
|
message: 'primary password cannot be the same as secondary password'
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
secondaryPassword: string().length(64).hex()
|
||||||
|
.when(['primaryPassword'], ([primary], schema) => {
|
||||||
|
if (!primary) return schema.required('required if primary password not set')
|
||||||
|
return schema.test({
|
||||||
|
test: secondary => primary !== secondary,
|
||||||
|
message: 'secondary password cannot be the same as primary password'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, ['primaryPassword', 'secondaryPassword'])
|
||||||
|
|
||||||
export const bioSchema = object({
|
export const bioSchema = object({
|
||||||
bio: string().required('required').trim()
|
bio: string().required('required').trim()
|
||||||
})
|
})
|
||||||
|
@ -257,9 +257,9 @@ export default function Settings ({ ssrData }) {
|
|||||||
label={
|
label={
|
||||||
<div className='d-flex align-items-center'>disable freebies
|
<div className='d-flex align-items-center'>disable freebies
|
||||||
<Info>
|
<Info>
|
||||||
<p>Some posts and comments can be created without paying. However, that content has limited visibility.</p>
|
<p>Some comments can be created without paying. However, those comments have limited visibility.</p>
|
||||||
|
|
||||||
<p>If you disable freebies, you will always pay for your posts and comments and get standard visibility.</p>
|
<p>If you disable freebies, you will always pay for your comments and get standard visibility.</p>
|
||||||
|
|
||||||
<p>If you attach a sending wallet, we disable freebies for you unless you have checked/unchecked this value already.</p>
|
<p>If you attach a sending wallet, we disable freebies for you unless you have checked/unchecked this value already.</p>
|
||||||
</Info>
|
</Info>
|
||||||
|
24
prisma/migrations/20240821014115_phoenixd/migration.sql
Normal file
24
prisma/migrations/20240821014115_phoenixd/migration.sql
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "WalletType" ADD VALUE 'PHOENIXD';
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "WalletPhoenixd" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"walletId" INTEGER NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"url" TEXT NOT NULL,
|
||||||
|
"secondaryPassword" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "WalletPhoenixd_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "WalletPhoenixd_walletId_key" ON "WalletPhoenixd"("walletId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "WalletPhoenixd" ADD CONSTRAINT "WalletPhoenixd_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "Wallet"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
CREATE TRIGGER wallet_phoenixd_as_jsonb
|
||||||
|
AFTER INSERT OR UPDATE ON "WalletPhoenixd"
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE wallet_wallet_type_as_jsonb();
|
@ -171,6 +171,7 @@ enum WalletType {
|
|||||||
CLN
|
CLN
|
||||||
LNBITS
|
LNBITS
|
||||||
NWC
|
NWC
|
||||||
|
PHOENIXD
|
||||||
}
|
}
|
||||||
|
|
||||||
model Wallet {
|
model Wallet {
|
||||||
@ -196,6 +197,7 @@ model Wallet {
|
|||||||
walletCLN WalletCLN?
|
walletCLN WalletCLN?
|
||||||
walletLNbits WalletLNbits?
|
walletLNbits WalletLNbits?
|
||||||
walletNWC WalletNWC?
|
walletNWC WalletNWC?
|
||||||
|
walletPhoenixd WalletPhoenixd?
|
||||||
withdrawals Withdrawl[]
|
withdrawals Withdrawl[]
|
||||||
InvoiceForward InvoiceForward[]
|
InvoiceForward InvoiceForward[]
|
||||||
|
|
||||||
@ -264,6 +266,16 @@ model WalletNWC {
|
|||||||
nwcUrlRecv String
|
nwcUrlRecv String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model WalletPhoenixd {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
walletId Int @unique
|
||||||
|
wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||||
|
url String
|
||||||
|
secondaryPassword String
|
||||||
|
}
|
||||||
|
|
||||||
model Mute {
|
model Mute {
|
||||||
muterId Int
|
muterId Int
|
||||||
mutedId Int
|
mutedId Int
|
||||||
|
184
scripts/genwallet.sh
Normal file
184
scripts/genwallet.sh
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
.__ .__ __
|
||||||
|
____ ____ ______ _ _______ | | | | _____/ |_
|
||||||
|
/ ___\_/ __ \ / \ \/ \/ /\__ \ | | | | _/ __ \ __\\
|
||||||
|
/ /_/ > ___/| | \ / / __ \| |_| |_\ ___/| |
|
||||||
|
\___ / \___ >___| /\/\_/ (____ /____/____/\___ >__|
|
||||||
|
/_____/ \/ \/ \/ \/
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
error () {
|
||||||
|
echo -n "error: $1"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
wallet=$1
|
||||||
|
[ -z $wallet ] && read -p "Enter wallet name: " wallet
|
||||||
|
[ -z $wallet ] && error "name required"
|
||||||
|
|
||||||
|
# default is wallet in UPPERCASE
|
||||||
|
walletType="${wallet^^}"
|
||||||
|
read -p "Enter walletType (default $walletType): " _walletType
|
||||||
|
if [ ! -z $_walletType ]; then
|
||||||
|
walletType=$_walletType
|
||||||
|
fi
|
||||||
|
|
||||||
|
# default is wallet capitalized with "wallet" prefix
|
||||||
|
walletField="wallet${wallet^}"
|
||||||
|
read -p "Enter walletField (default $walletField): " _walletField
|
||||||
|
if [ ! -z $_walletField ]; then
|
||||||
|
walletField=$_walletField
|
||||||
|
fi
|
||||||
|
|
||||||
|
# exit on first failed command
|
||||||
|
set -e
|
||||||
|
|
||||||
|
todo() {
|
||||||
|
echo "// $wallet::TODO"
|
||||||
|
}
|
||||||
|
|
||||||
|
# create folder and index.js
|
||||||
|
mkdir -p wallets/$wallet
|
||||||
|
cat > wallets/$wallet/index.js <<EOF
|
||||||
|
$(todo)
|
||||||
|
// create validation schema for wallet and import here
|
||||||
|
// import { ${wallet}Schema } from '@/lib/validate'
|
||||||
|
|
||||||
|
export const name = '$wallet'
|
||||||
|
|
||||||
|
$(todo)
|
||||||
|
// configure wallet fields
|
||||||
|
export const fields = []
|
||||||
|
|
||||||
|
$(todo)
|
||||||
|
// configure wallet card
|
||||||
|
export const card = {
|
||||||
|
title: '$wallet',
|
||||||
|
subtitle: '',
|
||||||
|
badges: []
|
||||||
|
}
|
||||||
|
|
||||||
|
$(todo)
|
||||||
|
// set validation schema
|
||||||
|
export const fieldValidation = null // ${wallet}Schema
|
||||||
|
|
||||||
|
export const walletType = '$walletType'
|
||||||
|
|
||||||
|
export const walletField = '$walletField'
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# create client.js
|
||||||
|
cat > wallets/$wallet/client.js <<EOF
|
||||||
|
export * from 'wallets/$wallet'
|
||||||
|
|
||||||
|
export async function testSendPayment (config, { logger }) {
|
||||||
|
$(todo)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendPayment (bolt11, config) {
|
||||||
|
$(todo)
|
||||||
|
}
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# create server.js
|
||||||
|
cat > wallets/$wallet/server.js <<EOF
|
||||||
|
export * from 'wallets/$wallet'
|
||||||
|
|
||||||
|
export async function testCreateInvoice (config) {
|
||||||
|
$(todo)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createInvoice (
|
||||||
|
{ msats, description, descriptionHash, expiry },
|
||||||
|
config
|
||||||
|
) {
|
||||||
|
$(todo)
|
||||||
|
}
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# add TODOs where manual update is needed
|
||||||
|
fragments=fragments/wallet.js
|
||||||
|
i=0
|
||||||
|
grep -n "// XXX \[WALLET\]" $fragments | while read -r match;
|
||||||
|
do
|
||||||
|
lineno=$(echo $match | cut -d':' -f1)
|
||||||
|
sed -i "$((lineno+i))i $(todo)" $fragments
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
|
||||||
|
client=wallets/client.js
|
||||||
|
lineno=$(grep -n "export default" $client | cut -d':' -f1)
|
||||||
|
sed -i "${lineno}i $(todo)" $client
|
||||||
|
|
||||||
|
server=wallets/server.js
|
||||||
|
lineno=$(grep -n "export default" $server | cut -d':' -f1)
|
||||||
|
sed -i "${lineno}i $(todo)" $server
|
||||||
|
|
||||||
|
# need to disable exit on failure since we run grep to check its exit code
|
||||||
|
set +e
|
||||||
|
|
||||||
|
# check if prisma/schema.prisma needs patch
|
||||||
|
schema=prisma/schema.prisma
|
||||||
|
grep --quiet "$walletField" $schema
|
||||||
|
if [ $? -eq 1 ]; then
|
||||||
|
tablename=${walletField^}
|
||||||
|
# find line to insert walletField in wallet model
|
||||||
|
lineno=$(grep -n "model Wallet {" $schema | cut -d':' -f1)
|
||||||
|
offset=$(tail -n +$lineno $schema | grep -nm 1 "}" | cut -d':' -f1)
|
||||||
|
offset=$(tail -n +$lineno $schema | head -n $offset | grep -nE "wallet[[:alpha:]]+\s+ Wallet[[:alpha:]]" | cut -d':' -f1 | tail -n1)
|
||||||
|
sed -i "$((lineno+offset))i\ \ $walletField $tablename?" $schema
|
||||||
|
|
||||||
|
# find line to insert model for wallet
|
||||||
|
lineno=$(grep -nE "model Wallet[[:alpha:]]+ {" $schema | cut -d':' -f1 | tail -n1)
|
||||||
|
offset=$(tail -n +$((lineno+1)) $schema | grep -nm 1 "{" | cut -d':' -f1)
|
||||||
|
i=$((lineno+offset))
|
||||||
|
sed -i "${i}i $(todo)" $schema
|
||||||
|
sed -i "$((i+1))i model Wallet${wallet^} {\n" $schema
|
||||||
|
sed -i "$((i+2))i\ \ id Int @id @default(autoincrement())" $schema
|
||||||
|
sed -i "$((i+3))i\ \ walletId Int @unique" $schema
|
||||||
|
sed -i "$((i+4))i\ \ wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)" $schema
|
||||||
|
sed -i "$((i+5))i\ \ createdAt DateTime @default(now()) @map(\"created_at\")" $schema
|
||||||
|
sed -i "$((i+6))i\ \ updatedAt DateTime @default(now()) @updatedAt @map(\"updated_at\")" $schema
|
||||||
|
sed -i "$((i+7))i }" $schema
|
||||||
|
|
||||||
|
# find line to insert wallet type
|
||||||
|
lineno=$(grep -nE "enum WalletType {" $schema | cut -d':' -f1)
|
||||||
|
offset=$(tail -n +$lineno $schema | grep -nm 1 "}" | cut -d':' -f1)
|
||||||
|
i=$((lineno+offset-1))
|
||||||
|
sed -i "${i} i\ \ ${walletType}" $schema
|
||||||
|
|
||||||
|
# create migration file with TODOs
|
||||||
|
migrationDir="prisma/migrations/$(date +%Y%m%d%H%M%S_$wallet)"
|
||||||
|
mkdir -p $migrationDir
|
||||||
|
cat > $migrationDir/migration.sql <<EOF
|
||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "WalletType" ADD VALUE '${walletType}';
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "$tablename" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"walletId" INTEGER NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
-- $wallet::TODO
|
||||||
|
|
||||||
|
CONSTRAINT "${tablename}_pkey" PRIMARY KEY ("int")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "${tablename}_walletId_key" ON "$tablename"("walletId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "$tablename" ADD CONSTRAINT "${tablename}_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "Wallet"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
CREATE TRIGGER wallet_${wallet}_as_jsonb
|
||||||
|
AFTER INSERT OR UPDATE ON "$tablename"
|
||||||
|
FOR EACH ROW EXECUTE PROCEDURE wallet_wallet_type_as_jsonb();
|
||||||
|
EOF
|
||||||
|
fi
|
@ -6,5 +6,6 @@ import * as cln from 'wallets/cln/client'
|
|||||||
import * as lnd from 'wallets/lnd/client'
|
import * as lnd from 'wallets/lnd/client'
|
||||||
import * as webln from 'wallets/webln/client'
|
import * as webln from 'wallets/webln/client'
|
||||||
import * as blink from 'wallets/blink/client'
|
import * as blink from 'wallets/blink/client'
|
||||||
|
import * as phoenixd from 'wallets/phoenixd/client'
|
||||||
|
|
||||||
export default [nwc, lnbits, lnc, lnAddr, cln, lnd, webln, blink]
|
export default [nwc, lnbits, lnc, lnAddr, cln, lnd, webln, blink, phoenixd]
|
||||||
|
39
wallets/phoenixd/client.js
Normal file
39
wallets/phoenixd/client.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
export * from 'wallets/phoenixd'
|
||||||
|
|
||||||
|
export async function testSendPayment (config, { logger }) {
|
||||||
|
// TODO:
|
||||||
|
// Not sure which endpoint to call to test primary password
|
||||||
|
// see https://phoenix.acinq.co/server/api
|
||||||
|
// Maybe just wait until test payments with HODL invoices?
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendPayment (bolt11, { url, primaryPassword }) {
|
||||||
|
// https://phoenix.acinq.co/server/api#pay-bolt11-invoice
|
||||||
|
const path = '/payinvoice'
|
||||||
|
|
||||||
|
const headers = new Headers()
|
||||||
|
headers.set('Authorization', 'Basic ' + Buffer.from(':' + primaryPassword).toString('base64'))
|
||||||
|
headers.set('Content-type', 'application/x-www-form-urlencoded')
|
||||||
|
|
||||||
|
const body = new URLSearchParams()
|
||||||
|
body.append('invoice', bolt11)
|
||||||
|
|
||||||
|
const res = await fetch(url + path, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.text()
|
||||||
|
throw new Error(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const payment = await res.json()
|
||||||
|
const preimage = payment.paymentPreimage
|
||||||
|
if (!preimage) {
|
||||||
|
throw new Error(payment.reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
return payment.paymentPreimage
|
||||||
|
}
|
45
wallets/phoenixd/index.js
Normal file
45
wallets/phoenixd/index.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { phoenixdSchema } from '@/lib/validate'
|
||||||
|
|
||||||
|
export const name = 'phoenixd'
|
||||||
|
|
||||||
|
// configure wallet fields
|
||||||
|
export const fields = [
|
||||||
|
{
|
||||||
|
name: 'url',
|
||||||
|
label: 'url',
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'primaryPassword',
|
||||||
|
label: 'primary password',
|
||||||
|
type: 'password',
|
||||||
|
optional: 'for sending',
|
||||||
|
help: 'You can find the primary password as `http-password` in your phoenixd configuration file as mentioned [here](https://phoenix.acinq.co/server/api#security).',
|
||||||
|
clientOnly: true,
|
||||||
|
editable: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'secondaryPassword',
|
||||||
|
label: 'secondary password',
|
||||||
|
type: 'password',
|
||||||
|
optional: 'for receiving',
|
||||||
|
help: 'You can find the secondary password as `http-password-limited-access` in your phoenixd configuration file as mentioned [here](https://phoenix.acinq.co/server/api#security).',
|
||||||
|
serverOnly: true,
|
||||||
|
editable: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// configure wallet card
|
||||||
|
export const card = {
|
||||||
|
title: 'phoenixd',
|
||||||
|
subtitle: 'use [phoenixd](https://phoenix.acinq.co/server) for payments',
|
||||||
|
badges: ['send & receive']
|
||||||
|
}
|
||||||
|
|
||||||
|
// phoenixd::TODO
|
||||||
|
// set validation schema
|
||||||
|
export const fieldValidation = phoenixdSchema
|
||||||
|
|
||||||
|
export const walletType = 'PHOENIXD'
|
||||||
|
|
||||||
|
export const walletField = 'walletPhoenixd'
|
38
wallets/phoenixd/server.js
Normal file
38
wallets/phoenixd/server.js
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { msatsToSats } from '@/lib/format'
|
||||||
|
|
||||||
|
export * from 'wallets/phoenixd'
|
||||||
|
|
||||||
|
export async function testCreateInvoice ({ url, secondaryPassword }) {
|
||||||
|
return await createInvoice(
|
||||||
|
{ msats: 1000, description: 'SN test invoice', expiry: 1 },
|
||||||
|
{ url, secondaryPassword })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createInvoice (
|
||||||
|
{ msats, description, descriptionHash, expiry },
|
||||||
|
{ url, secondaryPassword }
|
||||||
|
) {
|
||||||
|
// https://phoenix.acinq.co/server/api#create-bolt11-invoice
|
||||||
|
const path = '/createinvoice'
|
||||||
|
|
||||||
|
const headers = new Headers()
|
||||||
|
headers.set('Authorization', 'Basic ' + Buffer.from(':' + secondaryPassword).toString('base64'))
|
||||||
|
headers.set('Content-type', 'application/x-www-form-urlencoded')
|
||||||
|
|
||||||
|
const body = new URLSearchParams()
|
||||||
|
body.append('description', description)
|
||||||
|
body.append('amountSat', msatsToSats(msats))
|
||||||
|
|
||||||
|
const res = await fetch(url + path, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.text()
|
||||||
|
throw new Error(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const payment = await res.json()
|
||||||
|
return payment.serialized
|
||||||
|
}
|
@ -3,12 +3,13 @@ import * as cln from 'wallets/cln/server'
|
|||||||
import * as lnAddr from 'wallets/lightning-address/server'
|
import * as lnAddr from 'wallets/lightning-address/server'
|
||||||
import * as lnbits from 'wallets/lnbits/server'
|
import * as lnbits from 'wallets/lnbits/server'
|
||||||
import * as nwc from 'wallets/nwc/server'
|
import * as nwc from 'wallets/nwc/server'
|
||||||
|
import * as phoenixd from 'wallets/phoenixd/server'
|
||||||
import { addWalletLog } from '@/api/resolvers/wallet'
|
import { addWalletLog } from '@/api/resolvers/wallet'
|
||||||
import walletDefs from 'wallets/server'
|
import walletDefs from 'wallets/server'
|
||||||
import { parsePaymentRequest } from 'ln-service'
|
import { parsePaymentRequest } from 'ln-service'
|
||||||
import { toPositiveNumber } from '@/lib/validate'
|
import { toPositiveNumber } from '@/lib/validate'
|
||||||
import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
|
import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
|
||||||
export default [lnd, cln, lnAddr, lnbits, nwc]
|
export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd]
|
||||||
|
|
||||||
const MAX_PENDING_INVOICES_PER_WALLET = 25
|
const MAX_PENDING_INVOICES_PER_WALLET = 25
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user