Allow to choose price when configuring subscription
This commit is contained in:
parent
b856f23eb1
commit
f346315296
4 changed files with 107 additions and 33 deletions
|
@ -4,17 +4,35 @@ import { DateTime } from "luxon"
|
||||||
|
|
||||||
import { BACKEND_URL } from "@/constants"
|
import { BACKEND_URL } from "@/constants"
|
||||||
import { ethereumAddressMatch, EthereumSignature } from "@/utils/ethereum"
|
import { ethereumAddressMatch, EthereumSignature } from "@/utils/ethereum"
|
||||||
import { roundBigNumber } from "@/utils/numbers"
|
import { floatToBigNumber, roundBigNumber } from "@/utils/numbers"
|
||||||
import { http } from "./common"
|
import { http } from "./common"
|
||||||
import { Contracts, getContract } from "./contracts"
|
import { Contracts, getContract } from "./contracts"
|
||||||
|
|
||||||
|
const SECONDS_IN_DAY = 3600 * 24
|
||||||
|
const SECONDS_IN_MONTH = SECONDS_IN_DAY * 30
|
||||||
|
|
||||||
|
export async function getPricePerSec(
|
||||||
|
contractAddress: string,
|
||||||
|
signer: Signer,
|
||||||
|
pricePerMonth: number,
|
||||||
|
): Promise<BigNumber> {
|
||||||
|
const adapter = await getContract(Contracts.Adapter, contractAddress, signer)
|
||||||
|
const tokenAddress = await adapter.subscriptionToken()
|
||||||
|
const token = await getContract(Contracts.ERC20, tokenAddress, signer)
|
||||||
|
const tokenDecimals = await token.decimals()
|
||||||
|
const pricePerMonthInt = floatToBigNumber(pricePerMonth, tokenDecimals)
|
||||||
|
return pricePerMonthInt.div(SECONDS_IN_MONTH)
|
||||||
|
}
|
||||||
|
|
||||||
export async function getSubscriptionAuthorization(
|
export async function getSubscriptionAuthorization(
|
||||||
authToken: string,
|
authToken: string,
|
||||||
|
pricePerSec: BigNumber,
|
||||||
): Promise<EthereumSignature> {
|
): Promise<EthereumSignature> {
|
||||||
const url = `${BACKEND_URL}/api/v1/accounts/authorize_subscription`
|
const url = `${BACKEND_URL}/api/v1/accounts/authorize_subscription`
|
||||||
const response = await http(url, {
|
const response = await http(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
authToken,
|
authToken,
|
||||||
|
queryParams: { price: pricePerSec.toString() },
|
||||||
})
|
})
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
|
@ -28,11 +46,13 @@ export async function configureSubscription(
|
||||||
contractAddress: string,
|
contractAddress: string,
|
||||||
signer: Signer,
|
signer: Signer,
|
||||||
recipientAddress: string,
|
recipientAddress: string,
|
||||||
|
pricePerSec: BigNumber,
|
||||||
serverSignature: EthereumSignature,
|
serverSignature: EthereumSignature,
|
||||||
): Promise<TransactionResponse> {
|
): Promise<TransactionResponse> {
|
||||||
const adapter = await getContract(Contracts.Adapter, contractAddress, signer)
|
const adapter = await getContract(Contracts.Adapter, contractAddress, signer)
|
||||||
const transaction = await adapter.configureSubscription(
|
const transaction = await adapter.configureSubscription(
|
||||||
recipientAddress,
|
recipientAddress,
|
||||||
|
pricePerSec,
|
||||||
serverSignature.v,
|
serverSignature.v,
|
||||||
"0x" + serverSignature.r,
|
"0x" + serverSignature.r,
|
||||||
"0x" + serverSignature.s,
|
"0x" + serverSignature.s,
|
||||||
|
@ -40,9 +60,6 @@ export async function configureSubscription(
|
||||||
return transaction
|
return transaction
|
||||||
}
|
}
|
||||||
|
|
||||||
const SECONDS_IN_DAY = 3600 * 24
|
|
||||||
const SECONDS_IN_MONTH = SECONDS_IN_DAY * 30
|
|
||||||
|
|
||||||
export class Subscription {
|
export class Subscription {
|
||||||
|
|
||||||
recipientAddress: string;
|
recipientAddress: string;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
|
<h1>Configure subscription</h1>
|
||||||
<div class="subscription">
|
<div class="subscription">
|
||||||
<h1>Configure subscription</h1>
|
|
||||||
<div class="connect-wallet" v-if="canConnectWallet()">
|
<div class="connect-wallet" v-if="canConnectWallet()">
|
||||||
<button class="btn" @click="connectWallet()">Connect wallet</button>
|
<button class="btn" @click="connectWallet()">Connect wallet</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,14 +15,23 @@
|
||||||
<div>Price of one month: {{ subscription.pricePerMonth }}</div>
|
<div>Price of one month: {{ subscription.pricePerMonth }}</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
Subscription is not configured.
|
Subscription is not configured
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="setup" v-if="canConfigureSubscription()">
|
<form class="setup" v-if="canConfigureSubscription()">
|
||||||
<button class="btn" @click="onConfigureSubscription()">
|
<div class="price">
|
||||||
|
<label for="price">Price</label>
|
||||||
|
<input type="number" id="price" v-model="subscriptionPrice" min="0.00">
|
||||||
|
<span>per month</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn primary"
|
||||||
|
:disabled="subscriptionPrice <= 0"
|
||||||
|
@click.prevent="onConfigureSubscription()"
|
||||||
|
>
|
||||||
Set up subscription
|
Set up subscription
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</form>
|
||||||
<div class="withdraw" v-if="subscription !== null">
|
<div class="withdraw" v-if="subscription !== null">
|
||||||
<input v-model="subscriberAddress" placeholder="Subscriber address">
|
<input v-model="subscriberAddress" placeholder="Subscriber address">
|
||||||
<button class="btn" @click="onCheckSubsciptionState()">Check</button>
|
<button class="btn" @click="onCheckSubsciptionState()">Check</button>
|
||||||
|
@ -40,6 +49,7 @@ import { $, $$, $ref } from "vue/macros"
|
||||||
import { getVerifiedEthereumAddress, Profile } from "@/api/users"
|
import { getVerifiedEthereumAddress, Profile } from "@/api/users"
|
||||||
import {
|
import {
|
||||||
configureSubscription,
|
configureSubscription,
|
||||||
|
getPricePerSec,
|
||||||
getSubscriptionAuthorization,
|
getSubscriptionAuthorization,
|
||||||
getSubscriptionInfo,
|
getSubscriptionInfo,
|
||||||
getSubscriptionState,
|
getSubscriptionState,
|
||||||
|
@ -57,10 +67,11 @@ const props = defineProps<{
|
||||||
profile: Profile,
|
profile: Profile,
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { currentUser, ensureAuthToken } = $(useCurrentUser())
|
const { ensureAuthToken } = $(useCurrentUser())
|
||||||
const { instance } = $(useInstanceInfo())
|
const { instance } = $(useInstanceInfo())
|
||||||
const { connectWallet: connectEthereumWallet, getSigner } = useWallet()
|
const { connectWallet: connectEthereumWallet, getSigner } = useWallet()
|
||||||
const profileEthereumAddress = getVerifiedEthereumAddress(props.profile)
|
const profileEthereumAddress = getVerifiedEthereumAddress(props.profile)
|
||||||
|
const subscriptionPrice = $ref<number>(1)
|
||||||
let { walletAddress, walletError } = $(useWallet())
|
let { walletAddress, walletError } = $(useWallet())
|
||||||
let subscriptionConfigured = $ref<boolean | null>(null)
|
let subscriptionConfigured = $ref<boolean | null>(null)
|
||||||
let subscription = $ref<Subscription | null>(null)
|
let subscription = $ref<Subscription | null>(null)
|
||||||
|
@ -128,28 +139,32 @@ async function checkSubscription() {
|
||||||
|
|
||||||
function canConfigureSubscription(): boolean {
|
function canConfigureSubscription(): boolean {
|
||||||
return (
|
return (
|
||||||
Boolean(currentUser?.wallet_address) &&
|
profileEthereumAddress !== null &&
|
||||||
subscriptionConfigured === false
|
subscriptionConfigured === false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onConfigureSubscription() {
|
async function onConfigureSubscription() {
|
||||||
if (
|
if (
|
||||||
!currentUser ||
|
profileEthereumAddress === null ||
|
||||||
!currentUser.wallet_address ||
|
|
||||||
!instance ||
|
!instance ||
|
||||||
!instance.blockchain_contract_address
|
!instance.blockchain_contract_address
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Subscription configuration tx can be sent from any address
|
|
||||||
const signer = getSigner()
|
const signer = getSigner()
|
||||||
const authToken = ensureAuthToken()
|
const authToken = ensureAuthToken()
|
||||||
const signature = await getSubscriptionAuthorization(authToken)
|
const pricePerSec = await getPricePerSec(
|
||||||
|
instance.blockchain_contract_address,
|
||||||
|
signer,
|
||||||
|
subscriptionPrice,
|
||||||
|
)
|
||||||
|
const signature = await getSubscriptionAuthorization(authToken, pricePerSec)
|
||||||
const transaction = await configureSubscription(
|
const transaction = await configureSubscription(
|
||||||
instance.blockchain_contract_address,
|
instance.blockchain_contract_address,
|
||||||
signer,
|
signer,
|
||||||
currentUser.wallet_address,
|
profileEthereumAddress,
|
||||||
|
pricePerSec,
|
||||||
signature,
|
signature,
|
||||||
)
|
)
|
||||||
await transaction.wait()
|
await transaction.wait()
|
||||||
|
@ -157,7 +172,7 @@ async function onConfigureSubscription() {
|
||||||
subscription = await getSubscriptionInfo(
|
subscription = await getSubscriptionInfo(
|
||||||
instance.blockchain_contract_address,
|
instance.blockchain_contract_address,
|
||||||
signer,
|
signer,
|
||||||
currentUser.wallet_address,
|
profileEthereumAddress,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,19 +216,38 @@ async function onWithdrawReceived() {
|
||||||
@import "../styles/theme";
|
@import "../styles/theme";
|
||||||
|
|
||||||
.subscription {
|
.subscription {
|
||||||
@include block-btn;
|
|
||||||
|
|
||||||
background-color: $block-background-color;
|
|
||||||
border-radius: $block-border-radius;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: $block-inner-padding / 2;
|
gap: $block-outer-padding;
|
||||||
margin-bottom: $block-outer-padding;
|
text-align: center;
|
||||||
padding: $block-inner-padding;
|
}
|
||||||
|
|
||||||
h1 {
|
.info {
|
||||||
font-size: 20px;
|
background-color: $block-background-color;
|
||||||
margin: 0;
|
border-radius: $block-border-radius;
|
||||||
|
padding: $block-inner-padding;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $block-inner-padding;
|
||||||
|
|
||||||
|
.price {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
font-size: 16px;
|
||||||
|
gap: $input-padding;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -221,10 +255,5 @@ async function onWithdrawReceived() {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: $block-inner-padding / 2;
|
gap: $block-inner-padding / 2;
|
||||||
|
|
||||||
input {
|
|
||||||
border: 1px solid $btn-background-color;
|
|
||||||
border-radius: $btn-border-radius;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -11,3 +11,21 @@ export function roundBigNumber(value: BigNumber, precision: number): BigNumber {
|
||||||
return value.div(divisor).mul(divisor)
|
return value.div(divisor).mul(divisor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPrecision(value: number): number {
|
||||||
|
if (!isFinite(value)) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
let precision = 0
|
||||||
|
while (Math.round(value * Math.pow(10, precision)) / Math.pow(10, precision) !== value) {
|
||||||
|
precision++
|
||||||
|
}
|
||||||
|
return precision
|
||||||
|
}
|
||||||
|
|
||||||
|
export function floatToBigNumber(value: number, decimals: number): BigNumber {
|
||||||
|
const precision = getPrecision(value)
|
||||||
|
const denominator = 10 ** precision
|
||||||
|
const numerator = Math.round(value * denominator)
|
||||||
|
return BigNumber.from(10).pow(decimals).mul(numerator).div(denominator)
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { expect } from "chai"
|
import { expect } from "chai"
|
||||||
import { BigNumber } from "@ethersproject/bignumber"
|
import { BigNumber } from "@ethersproject/bignumber"
|
||||||
import { roundBigNumber } from "@/utils/numbers"
|
import { floatToBigNumber, roundBigNumber } from "@/utils/numbers"
|
||||||
|
|
||||||
describe("Numbers utils", () => {
|
describe("Numbers utils", () => {
|
||||||
it("Should round big number", () => {
|
it("Should round big number", () => {
|
||||||
|
@ -11,4 +11,14 @@ describe("Numbers utils", () => {
|
||||||
expect(roundBigNumber(value, 5).toNumber()).to.equal(534990)
|
expect(roundBigNumber(value, 5).toNumber()).to.equal(534990)
|
||||||
expect(roundBigNumber(value, 6).toNumber()).to.equal(534985)
|
expect(roundBigNumber(value, 6).toNumber()).to.equal(534985)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("Should convert float to big number", () => {
|
||||||
|
const value1 = 3.94031726813
|
||||||
|
const value2 = 1
|
||||||
|
const decimals = 18
|
||||||
|
expect(floatToBigNumber(value1, decimals).toString())
|
||||||
|
.to.equal("3940317268130000000")
|
||||||
|
expect(floatToBigNumber(value2, decimals).toString())
|
||||||
|
.to.equal("1000000000000000000")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue