invoice listeners support on lnd and other fixes around wallets/

This commit is contained in:
fiatjaf 2020-10-02 17:13:33 -03:00
parent 90c640b659
commit b3c69ad49c
12 changed files with 217 additions and 151 deletions

View File

@ -35,24 +35,21 @@ LNBITS_ADMIN_MACAROON=LNBITS_ADMIN_MACAROON
# LndWallet
LND_GRPC_ENDPOINT=127.0.0.1
LND_GRPC_PORT=11009
LND_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
LND_GRPC_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
LND_ADMIN_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/admin.macaroon"
LND_INVOICE_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/invoice.macaroon"
LND_READ_MACAROON="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/read.macaroon"
# LndRestWallet
LND_REST_ENDPOINT=https://localhost:8080/
LND_REST_CERT="/home/bob/.config/Zap/lnd/bitcoin/mainnet/wallet-1/data/chain/bitcoin/mainnet/tls.cert"
LND_REST_ADMIN_MACAROON="HEXSTRING"
LND_REST_INVOICE_MACAROON="HEXSTRING"
LND_REST_READ_MACAROON="HEXSTRING"
# LNPayWallet
LNPAY_API_ENDPOINT=https://lnpay.co/v1/
LNPAY_API_KEY=LNPAY_API_KEY
LNPAY_ADMIN_KEY=LNPAY_ADMIN_KEY
LNPAY_INVOICE_KEY=LNPAY_INVOICE_KEY
LNPAY_READ_KEY=LNPAY_READ_KEY
# LntxbotWallet
LNTXBOT_API_ENDPOINT=https://lntxbot.bigsun.xyz/

View File

@ -6,6 +6,7 @@ from .crud import get_pay_link_by_invoice, mark_webhook_sent
async def on_invoice_paid(payment: Payment) -> None:
print(payment)
islnurlp = "lnurlp" == payment.extra.get("tag")
if islnurlp:
pay_link = get_pay_link_by_invoice(payment.payment_hash)

View File

@ -1,5 +1,5 @@
from abc import ABC, abstractmethod
from typing import NamedTuple, Optional
from typing import NamedTuple, Optional, AsyncGenerator
class InvoiceResponse(NamedTuple):
@ -43,6 +43,15 @@ class Wallet(ABC):
def get_payment_status(self, checking_id: str) -> PaymentStatus:
pass
@abstractmethod
def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
"""
this is an async function, but here it is noted without the 'async'
prefix because mypy has a bug identifying the signature of abstract
methods.
"""
pass
class Unsupported(Exception):
pass

View File

@ -1,12 +1,12 @@
try:
from lightning import LightningRpc # type: ignore
from lightning import LightningRpc, RpcError # type: ignore
except ImportError: # pragma: nocover
LightningRpc = None
import random
from os import getenv
from typing import Optional
from typing import Optional, AsyncGenerator
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported
@ -15,26 +15,52 @@ class CLightningWallet(Wallet):
if LightningRpc is None: # pragma: nocover
raise ImportError("The `pylightning` library must be installed to use `CLightningWallet`.")
self.l1 = LightningRpc(getenv("CLIGHTNING_RPC"))
self.ln = LightningRpc(getenv("CLIGHTNING_RPC"))
# check description_hash support (could be provided by a plugin)
self.supports_description_hash = False
try:
answer = self.ln.help("invoicewithdescriptionhash")
if answer["help"][0]["command"].startswith(
"invoicewithdescriptionhash msatoshi label description_hash",
):
self.supports_description_hash = True
except:
pass
# check last payindex so we can listen from that point on
self.last_pay_index = 0
invoices = self.ln.listinvoices()
if len(invoices["invoices"]):
self.last_pay_index = invoices["invoices"][-1]["pay_index"]
def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
) -> InvoiceResponse:
if description_hash:
raise Unsupported("description_hash")
label = "lbl{}".format(random.random())
r = self.l1.invoice(amount * 1000, label, memo, exposeprivatechannels=True)
ok, checking_id, payment_request, error_message = True, r["payment_hash"], r["bolt11"], None
return InvoiceResponse(ok, checking_id, payment_request, error_message)
msat = amount * 1000
try:
if description_hash:
if not self.supports_description_hash:
raise Unsupported("description_hash")
r = self.ln.call("invoicewithdescriptionhash", [msat, label, memo])
return InvoiceResponse(True, label, r["bolt11"], "")
else:
r = self.ln.invoice(msat, label, memo, exposeprivatechannels=True)
return InvoiceResponse(True, label, r["bolt11"], "")
except RpcError as exc:
error_message = f"lightningd '{exc.method}' failed with '{exc.error}'."
return InvoiceResponse(False, label, None, error_message)
def pay_invoice(self, bolt11: str) -> PaymentResponse:
r = self.l1.pay(bolt11)
r = self.ln.pay(bolt11)
ok, checking_id, fee_msat, error_message = True, r["payment_hash"], r["msatoshi_sent"] - r["msatoshi"], None
return PaymentResponse(ok, checking_id, fee_msat, error_message)
def get_invoice_status(self, checking_id: str) -> PaymentStatus:
r = self.l1.listinvoices(checking_id)
r = self.ln.listinvoices(checking_id)
if not r["invoices"]:
return PaymentStatus(False)
if r["invoices"][0]["label"] == checking_id:
@ -42,7 +68,7 @@ class CLightningWallet(Wallet):
raise KeyError("supplied an invalid checking_id")
def get_payment_status(self, checking_id: str) -> PaymentStatus:
r = self.l1.listpays(payment_hash=checking_id)
r = self.ln.listpays(payment_hash=checking_id)
if not r["pays"]:
return PaymentStatus(False)
if r["pays"][0]["payment_hash"] == checking_id:
@ -53,3 +79,9 @@ class CLightningWallet(Wallet):
return PaymentStatus(False)
return PaymentStatus(None)
raise KeyError("supplied an invalid checking_id")
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
while True:
paid = self.ln.waitanyinvoice(self.last_pay_index)
self.last_pay_index = paid["pay_index"]
yield paid["label"]

View File

@ -1,5 +1,5 @@
from os import getenv
from typing import Optional, Dict
from typing import Optional, Dict, AsyncGenerator
from requests import get, post
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
@ -64,3 +64,7 @@ class LNbitsWallet(Wallet):
return PaymentStatus(None)
return PaymentStatus(r.json()["paid"])
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
print("lnbits does not support paid invoices stream yet")
yield ""

View File

@ -5,88 +5,94 @@ except ImportError: # pragma: nocover
import base64
from os import getenv
from typing import Optional, Dict
from typing import Optional, Dict, AsyncGenerator
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
def parse_checking_id(checking_id: str) -> bytes:
return base64.b64decode(
checking_id.replace("_", "/"),
)
def stringify_checking_id(r_hash: bytes) -> str:
return (
base64.b64encode(
r_hash,
)
.decode("utf-8")
.replace("/", "_")
)
class LndWallet(Wallet):
def __init__(self):
if lnd_grpc is None: # pragma: nocover
raise ImportError("The `lnd-grpc` library must be installed to use `LndWallet`.")
endpoint = getenv("LND_GRPC_ENDPOINT")
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
self.port = getenv("LND_GRPC_PORT")
self.auth_admin = getenv("LND_ADMIN_MACAROON")
self.auth_invoice = getenv("LND_INVOICE_MACAROON")
self.auth_read = getenv("LND_READ_MACAROON")
self.auth_cert = getenv("LND_CERT")
endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
port = getenv("LND_GRPC_PORT")
cert = getenv("LND_GRPC_CERT") or getenv("LND_CERT")
auth_admin = getenv("LND_ADMIN_MACAROON")
auth_invoices = getenv("LND_INVOICE_MACAROON")
network = getenv("LND_GRPC_NETWORK", "mainnet")
self.admin_rpc = lnd_grpc.Client(
lnd_dir=None,
macaroon_path=auth_admin,
tls_cert_path=cert,
network=network,
grpc_host=endpoint,
grpc_port=port,
)
self.invoices_rpc = lnd_grpc.Client(
lnd_dir=None,
macaroon_path=auth_invoices,
tls_cert_path=cert,
network=network,
grpc_host=endpoint,
grpc_port=port,
)
def create_invoice(
self, amount: int, memo: Optional[str] = None, description_hash: Optional[bytes] = None
) -> InvoiceResponse:
lnd_rpc = lnd_grpc.Client(
lnd_dir=None,
macaroon_path=self.auth_invoice,
tls_cert_path=self.auth_cert,
network="mainnet",
grpc_host=self.endpoint,
grpc_port=self.port,
)
params: Dict = {"value": amount, "expiry": 600, "private": True}
if description_hash:
params["description_hash"] = description_hash # as bytes directly
else:
params["memo"] = memo or ""
lndResponse = lnd_rpc.add_invoice(**params)
decoded_hash = base64.b64encode(lndResponse.r_hash).decode("utf-8").replace("/", "_")
ok, checking_id, payment_request, error_message = True, decoded_hash, str(lndResponse.payment_request), None
return InvoiceResponse(ok, checking_id, payment_request, error_message)
resp = self.invoices_rpc.add_invoice(**params)
checking_id = stringify_checking_id(resp.r_hash)
payment_request = str(resp.payment_request)
return InvoiceResponse(True, checking_id, payment_request, None)
def pay_invoice(self, bolt11: str) -> PaymentResponse:
lnd_rpc = lnd_grpc.Client(
lnd_dir=None,
macaroon_path=self.auth_admin,
tls_cert_path=self.auth_cert,
network="mainnet",
grpc_host=self.endpoint,
grpc_port=self.port,
)
resp = self.admin_rpc.pay_invoice(payment_request=bolt11)
payinvoice = lnd_rpc.pay_invoice(
payment_request=bolt11,
)
if resp.payment_error:
return PaymentResponse(False, "", 0, resp.payment_error)
ok, checking_id, fee_msat, error_message = True, None, 0, None
if payinvoice.payment_error:
ok, error_message = False, payinvoice.payment_error
else:
checking_id = base64.b64encode(payinvoice.payment_hash).decode("utf-8").replace("/", "_")
return PaymentResponse(ok, checking_id, fee_msat, error_message)
checking_id = stringify_checking_id(resp.payment_hash)
return PaymentResponse(True, checking_id, 0, None)
def get_invoice_status(self, checking_id: str) -> PaymentStatus:
check_id = base64.b64decode(checking_id.replace("_", "/"))
print(check_id)
lnd_rpc = lnd_grpc.Client(
lnd_dir=None,
macaroon_path=self.auth_invoice,
tls_cert_path=self.auth_cert,
network="mainnet",
grpc_host=self.endpoint,
grpc_port=self.port,
)
for _response in lnd_rpc.subscribe_single_invoice(check_id):
r_hash = parse_checking_id(checking_id)
for _response in self.invoices_rpc.subscribe_single_invoice(r_hash):
if _response.state == 1:
return PaymentStatus(True)
return PaymentStatus(None)
def get_payment_status(self, checking_id: str) -> PaymentStatus:
return PaymentStatus(True)
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
for paid in self.invoices_rpc.SubscribeInvoices():
print("PAID", paid)
checking_id = stringify_checking_id(paid.r_hash)
yield checking_id

View File

@ -1,7 +1,10 @@
from os import getenv
from typing import Optional, Dict
import httpx
import json
import base64
from requests import get, post
from os import getenv
from typing import Optional, Dict, AsyncGenerator
from lnbits import bolt11
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
@ -12,10 +15,8 @@ class LndRestWallet(Wallet):
endpoint = getenv("LND_REST_ENDPOINT")
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
print(self.endpoint)
self.auth_admin = {"Grpc-Metadata-macaroon": getenv("LND_REST_ADMIN_MACAROON")}
self.auth_invoice = {"Grpc-Metadata-macaroon": getenv("LND_REST_INVOICE_MACAROON")}
self.auth_read = {"Grpc-Metadata-macaroon": getenv("LND_REST_READ_MACAROON")}
self.auth_cert = getenv("LND_REST_CERT")
def create_invoice(
@ -30,84 +31,97 @@ class LndRestWallet(Wallet):
else:
data["memo"] = memo or ""
r = post(
r = httpx.post(
url=f"{self.endpoint}/v1/invoices",
headers=self.auth_invoice,
verify=self.auth_cert,
json=data,
)
ok, checking_id, payment_request, error_message = r.ok, None, None, None
if r.is_error:
error_message = r.text
try:
error_message = r.json()["error"]
except Exception:
pass
return InvoiceResponse(False, None, None, error_message)
if r.ok:
data = r.json()
payment_request = data["payment_request"]
data = r.json()
payment_request = data["payment_request"]
payment_hash = base64.b64decode(data["r_hash"]).hex()
checking_id = payment_hash
r = get(
url=f"{self.endpoint}/v1/payreq/{payment_request}",
headers=self.auth_read,
verify=self.auth_cert,
)
print(r)
if r.ok:
checking_id = r.json()["payment_hash"].replace("/", "_")
print(checking_id)
error_message = None
ok = True
return InvoiceResponse(ok, checking_id, payment_request, error_message)
return InvoiceResponse(True, checking_id, payment_request, None)
def pay_invoice(self, bolt11: str) -> PaymentResponse:
r = post(
r = httpx.post(
url=f"{self.endpoint}/v1/channels/transactions",
headers=self.auth_admin,
verify=self.auth_cert,
json={"payment_request": bolt11},
)
ok, checking_id, fee_msat, error_message = r.ok, None, 0, None
r = get(
url=f"{self.endpoint}/v1/payreq/{bolt11}",
headers=self.auth_admin,
verify=self.auth_cert,
)
if r.ok:
checking_id = r.json()["payment_hash"]
else:
error_message = r.json()["error"]
if r.is_error:
error_message = r.text
try:
error_message = r.json()["error"]
except:
pass
return PaymentResponse(False, None, 0, error_message)
return PaymentResponse(ok, checking_id, fee_msat, error_message)
payment_hash = r.json()["payment_hash"]
checking_id = payment_hash
return PaymentResponse(True, checking_id, 0, None)
def get_invoice_status(self, checking_id: str) -> PaymentStatus:
checking_id = checking_id.replace("_", "/")
print(checking_id)
r = get(
r = httpx.get(
url=f"{self.endpoint}/v1/invoice/{checking_id}",
headers=self.auth_invoice,
verify=self.auth_cert,
)
print(r.json()["settled"])
if not r or r.json()["settled"] == False:
return PaymentStatus(None)
return PaymentStatus(r.json()["settled"])
def get_payment_status(self, checking_id: str) -> PaymentStatus:
r = get(
r = httpx.get(
url=f"{self.endpoint}/v1/payments",
headers=self.auth_admin,
verify=self.auth_cert,
params={"include_incomplete": "True", "max_payments": "20"},
)
if not r.ok:
if r.is_error:
return PaymentStatus(None)
payments = [p for p in r.json()["payments"] if p["payment_hash"] == checking_id]
print(checking_id)
payment = payments[0] if payments else None
# check payment.status: https://api.lightning.community/rest/index.html?python#peersynctype
# check payment.status:
# https://api.lightning.community/rest/index.html?python#peersynctype
statuses = {"UNKNOWN": None, "IN_FLIGHT": None, "SUCCEEDED": True, "FAILED": False}
return PaymentStatus(statuses[payment["status"]])
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
url = self.endpoint + "/v1/invoices/subscribe"
async with httpx.AsyncClient(timeout=None, headers=self.auth_admin, verify=self.auth_cert) as client:
async with client.stream("GET", url) as r:
print("ok")
print(r)
print(r.is_error)
print("ok")
async for line in r.aiter_lines():
print("line", line)
try:
event = json.loads(line)["result"]
print(event)
except:
continue
payment_hash = bolt11.decode(event["payment_request"]).payment_hash
yield payment_hash

View File

@ -4,7 +4,6 @@ import httpx
from os import getenv
from http import HTTPStatus
from typing import Optional, Dict, AsyncGenerator
from requests import get, post
from quart import request
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
@ -14,11 +13,9 @@ class LNPayWallet(Wallet):
"""https://docs.lnpay.co/"""
def __init__(self):
endpoint = getenv("LNPAY_API_ENDPOINT")
endpoint = getenv("LNPAY_API_ENDPOINT", "https://lnpay.co/v1")
self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint
self.auth_admin = getenv("LNPAY_ADMIN_KEY")
self.auth_invoice = getenv("LNPAY_INVOICE_KEY")
self.auth_read = getenv("LNPAY_READ_KEY")
self.auth_api = {"X-Api-Key": getenv("LNPAY_API_KEY")}
def create_invoice(
@ -33,8 +30,8 @@ class LNPayWallet(Wallet):
else:
data["memo"] = memo or ""
r = post(
url=f"{self.endpoint}/user/wallet/{self.auth_invoice}/invoice",
r = httpx.post(
url=f"{self.endpoint}/user/wallet/{self.auth_admin}/invoice",
headers=self.auth_api,
json=data,
)
@ -52,7 +49,7 @@ class LNPayWallet(Wallet):
return InvoiceResponse(ok, checking_id, payment_request, error_message)
def pay_invoice(self, bolt11: str) -> PaymentResponse:
r = post(
r = httpx.post(
url=f"{self.endpoint}/user/wallet/{self.auth_admin}/withdraw",
headers=self.auth_api,
json={"payment_request": bolt11},
@ -68,12 +65,12 @@ class LNPayWallet(Wallet):
return self.get_payment_status(checking_id)
def get_payment_status(self, checking_id: str) -> PaymentStatus:
r = get(
r = httpx.get(
url=f"{self.endpoint}/user/lntx/{checking_id}?fields=settled",
headers=self.auth_api,
)
if not r.ok:
if r.is_error:
return PaymentStatus(None)
statuses = {0: None, 1: True, -1: False}
@ -82,8 +79,7 @@ class LNPayWallet(Wallet):
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
self.queue: asyncio.Queue = asyncio.Queue()
while True:
item = await self.queue.get()
yield item
yield await self.queue.get()
self.queue.task_done()
async def webhook_listener(self):

View File

@ -1,5 +1,5 @@
from os import getenv
from typing import Optional, Dict
from typing import Optional, Dict, AsyncGenerator
from requests import post
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet
@ -75,3 +75,7 @@ class LntxbotWallet(Wallet):
statuses = {"complete": True, "failed": False, "pending": None, "unknown": None}
return PaymentStatus(statuses[r.json().get("status", "unknown")])
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
print("lntxbot does not support paid invoices stream yet")
yield ""

View File

@ -1,10 +1,10 @@
import json
import asyncio
import hmac
import httpx
from http import HTTPStatus
from os import getenv
from typing import Optional, AsyncGenerator
from requests import get, post
from quart import request, url_for
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported
@ -25,8 +25,8 @@ class OpenNodeWallet(Wallet):
if description_hash:
raise Unsupported("description_hash")
r = post(
url=f"{self.endpoint}/v1/charges",
r = httpx.post(
f"{self.endpoint}/v1/charges",
headers=self.auth_invoice,
json={
"amount": amount,
@ -34,42 +34,43 @@ class OpenNodeWallet(Wallet):
"callback_url": url_for("webhook_listener", _external=True),
},
)
ok, checking_id, payment_request, error_message = r.ok, None, None, None
if r.ok:
data = r.json()["data"]
checking_id = data["id"]
payment_request = data["lightning_invoice"]["payreq"]
else:
if r.is_error:
error_message = r.json()["message"]
return InvoiceResponse(False, None, None, error_message)
return InvoiceResponse(ok, checking_id, payment_request, error_message)
data = r.json()["data"]
checking_id = data["id"]
payment_request = data["lightning_invoice"]["payreq"]
return InvoiceResponse(True, checking_id, payment_request, error_message)
def pay_invoice(self, bolt11: str) -> PaymentResponse:
r = post(url=f"{self.endpoint}/v2/withdrawals", headers=self.auth_admin, json={"type": "ln", "address": bolt11})
ok, checking_id, fee_msat, error_message = r.ok, None, 0, None
r = httpx.post(
f"{self.endpoint}/v2/withdrawals", headers=self.auth_admin, json={"type": "ln", "address": bolt11}
)
if r.ok:
data = r.json()["data"]
checking_id, fee_msat = data["id"], data["fee"] * 1000
else:
if r.is_error:
error_message = r.json()["message"]
return PaymentResponse(False, None, 0, error_message)
return PaymentResponse(ok, checking_id, fee_msat, error_message)
data = r.json()["data"]
checking_id, fee_msat = data["id"]
fee_msat = data["fee"] * 1000
return PaymentResponse(True, checking_id, fee_msat, error_message)
def get_invoice_status(self, checking_id: str) -> PaymentStatus:
r = get(url=f"{self.endpoint}/v1/charge/{checking_id}", headers=self.auth_invoice)
r = httpx.get(f"{self.endpoint}/v1/charge/{checking_id}", headers=self.auth_invoice)
if not r.ok:
if r.is_error:
return PaymentStatus(None)
statuses = {"processing": None, "paid": True, "unpaid": False}
return PaymentStatus(statuses[r.json()["data"]["status"]])
def get_payment_status(self, checking_id: str) -> PaymentStatus:
r = get(url=f"{self.endpoint}/v1/withdrawal/{checking_id}", headers=self.auth_admin)
r = httpx.get(f"{self.endpoint}/v1/withdrawal/{checking_id}", headers=self.auth_admin)
if not r.ok:
if r.is_error:
return PaymentStatus(None)
statuses = {"initial": None, "pending": None, "confirmed": True, "error": False, "failed": False}
@ -78,8 +79,7 @@ class OpenNodeWallet(Wallet):
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
self.queue: asyncio.Queue = asyncio.Queue()
while True:
item = await self.queue.get()
yield item
yield await self.queue.get()
self.queue.task_done()
async def webhook_listener(self):

View File

@ -34,7 +34,7 @@ class SparkWallet(Wallet):
data = r.json()
except:
raise UnknownError(r.text)
if not r.ok:
if r.is_error:
raise SparkError(data["message"])
return data
@ -96,7 +96,7 @@ class SparkWallet(Wallet):
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
url = self.url + "/stream?access-key=" + self.token
async with httpx.AsyncClient() as client:
async with httpx.AsyncClient(timeout=None) as client:
async with client.stream("GET", url) as r:
async for line in r.aiter_lines():
if line.startswith("data:"):

View File

@ -1,4 +1,4 @@
from typing import Optional
from typing import Optional, AsyncGenerator
from .base import InvoiceResponse, PaymentResponse, PaymentStatus, Wallet, Unsupported
@ -17,3 +17,6 @@ class VoidWallet(Wallet):
def get_payment_status(self, checking_id: str) -> PaymentStatus:
raise Unsupported("")
async def paid_invoices_stream(self) -> AsyncGenerator[str, None]:
yield ""